Chrome Extension Native Messaging — Best Practices

8 min read

Native Messaging Patterns

Overview

Native messaging enables Chrome extensions to communicate with native applications. Two modes:

Connection Management

Auto-Reconnecting Port

function createNativeConnection(appId) {
  const port = chrome.runtime.connectNative(appId);
  port.onMessage.addListener((message) => console.log(message));
  port.onDisconnect.addListener(() => setTimeout(() => createNativeConnection(appId), 5000));
  return port;
}

Backoff Reconnection

class NativeConnectionManager {
  constructor(appId) { this.appId = appId; this.retryCount = 0; }
  connect() {
    this.port = chrome.runtime.connectNative(this.appId);
    this.port.onDisconnect.addListener(() => this.attemptReconnection());
  }
  attemptReconnection() { if (this.retryCount >= 5) return; setTimeout(() => this.connect(), 1000 * Math.pow(2, this.retryCount++)); }
}

Heartbeat Detection

class HeartbeatConnection {
  constructor(appId, interval = 30000) { this.port = chrome.runtime.connectNative(appId); this.timer = setInterval(() => this.port?.postMessage({ type: 'heartbeat' }), interval); }
}

Request-Response Pattern

Promise Wrapper

function sendNativeMessage(appId, message, timeout = 30000) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('Timeout')), timeout);
    chrome.runtime.sendNativeMessage(appId, message, (response) => { clearTimeout(timer); chrome.runtime.lastError ? reject(new Error(chrome.runtime.lastError.message)) : resolve(response); });
  });
}

Message ID Correlation

class RequestResponseManager {
  constructor() { this.pending = new Map(); this.messageId = 0; }
  sendRequest(appId, message, timeout = 30000) {
    const id = ++this.messageId;
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`Request ${id} timed out`)); }, timeout);
      this.pending.set(id, { resolve, reject, timer });
      const port = chrome.runtime.connectNative(appId);
      port.postMessage({ ...message, requestId: id });
      port.onMessage.addListener((response) => { if (response.requestId === id) { clearTimeout(timer); this.pending.get(id)?.resolve(response); this.pending.delete(id); } });
    });
  }
}

Streaming Data

Chunking Large Payloads (1MB limit)

const MAX_SIZE = 1024 * 1024;
function sendLargeMessage(port, data) {
  const json = JSON.stringify(data), chunks = [];
  for (let i = 0; i < json.length; i += MAX_SIZE) chunks.push(json.slice(i, i + MAX_SIZE));
  port.postMessage({ type: 'chunked', total: chunks.length, chunks });
}

Progress Tracking

class ProgressConnection {
  constructor(appId, onProgress) { this.port = chrome.runtime.connectNative(appId); this.port.onMessage.addListener((msg) => { if (msg.type === 'progress') onProgress(msg.percent, msg.stage); }); }
}

Error Recovery

Host Not Found

async function checkNativeHost(appId) {
  return new Promise((resolve) => {
    const port = chrome.runtime.connectNative(appId);
    port.onDisconnect.addListener(() => resolve(!chrome.runtime.lastError?.message?.includes('not found')));
    port.postMessage({ type: 'ping' });
  });
}

Crash Recovery

async function sendWithRecovery(appId, message) {
  try { return await sendNativeMessage(appId, message); }
  catch (error) { if (error.message.includes('native host')) { await new Promise(r => setTimeout(r, 2000)); return sendWithRecovery(appId, message); } throw error; }
}

Security

Input Validation

function validateMessage(message) {
  if (!message || typeof message !== 'object') throw new Error('Invalid message format');
  const validTypes = ['response', 'progress', 'error', 'heartbeat'];
  if (!message.type || !validTypes.includes(message.type)) throw new Error('Invalid message type');
}
function handleMessage(message) {
  validateMessage(message);
  switch (message.action) { case 'updateBadge': chrome.action.setBadgeText({ text: message.value }); break; case 'showNotification': chrome.notifications.create(message.options); break; }
}

Structured Message Format

const request = { type: 'request', action: 'getData', requestId: Date.now(), payload: {} };
const response = { type: 'response', requestId: 123, data: {}, error: null };

Native Host Tips

32-bit Length Prefix Protocol {#32-bit-length-prefix-protocol}

import struct, sys, json
def read_message():
    length_bytes = sys.stdin.buffer.read(4)
    if not length_bytes: return None
    return json.loads(sys.stdin.buffer.read(struct.unpack('I', length_bytes)[0]).decode('utf-8'))
def write_message(message):
    json_str = json.dumps(message)
    sys.stdout.buffer.write(struct.pack('I', len(json_str)) + json_str.encode('utf-8'))
    sys.stdout.buffer.flush()
while (msg := read_message()) is not None: write_message(process(msg))

Host Installer Detection

async function detectHost(appId) { try { const port = chrome.runtime.connectNative(appId); port.disconnect(); return { installed: true }; } catch (error) { return { installed: false, instructions: 'Download from https://example.com/downloads' }; } }

Cross-references

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.