Skip to main content

Message Passing Protocol

Knowledge Tooltip uses Chrome’s message passing API to enable communication between the content script (UI layer) and background service worker (API layer). This is the bridge that makes the architecture work.

Why Message Passing?

Chrome extensions have isolated execution contexts:
  • Content scripts: Run in web page context, can access DOM
  • Background workers: Run in extension context, have elevated privileges
  • Web pages: Run in page context, completely isolated
Message passing allows these contexts to communicate safely without sharing memory or scope.
All message passing in Knowledge Tooltip uses chrome.runtime.sendMessage() and chrome.runtime.onMessage.addListener().

Communication Flow

Message Format

Request (Content Script → Background)

All messages follow this structure:
{
  action: string,        // Handler identifier
  ...params              // Action-specific parameters
}

Response (Background → Content Script)

Standardized response format:
{
  success: boolean,
  data?: any,           // Present if success === true
  error?: string        // Present if success === false
}
Consistent response format allows content script to handle all API calls uniformly.

Message Types

1. Fetch Wikipedia Summary

Request:
content.js:410
const response = await chrome.runtime.sendMessage({
  action: 'fetchWikipedia',
  term: 'Quantum Computing',
  language: 'en'
});
Handler:
background.js:22
if (message.action === 'fetchWikipedia') {
  fetchWikipediaData(message.term, message.language)
    .then(data => sendResponse({ success: true, data }))
    .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
  return true; // Keeps channel open for async response
}
Response:
{
  success: true,
  data: {
    title: "Quantum computing",
    extract: "Quantum computing is a type of computation...",
    thumbnail: { source: "https://...", width: 320, height: 213 },
    content_urls: { desktop: { page: "https://en.wikipedia.org/wiki/..." } }
  }
}

2. Search Wikipedia

Fallback when direct page fetch fails (404): Request:
content.js:419
const searchResult = await searchWikipedia(cleanedTerm, currentLanguage);

// Inside searchWikipedia function:
async function searchWikipedia(term, language) {
  const response = await chrome.runtime.sendMessage({
    action: 'searchWikipedia',
    term: term,
    language: language
  });

  if (response.success) {
    return response.result; // Returns best match title
  }
  return null;
}
Response:
{
  success: true,
  result: "Quantum computing" // Best matching article title
}

3. Fetch Wiktionary Definition

Request:
content.js:510
const wiktResponse = await chrome.runtime.sendMessage({
  action: 'fetchWiktionary',
  term: term.toLowerCase(),
  language: currentLanguage
});
Response:
{
  success: true,
  data: {
    en: [
      {
        partOfSpeech: "noun",
        definitions: [
          { definition: "A device for...", ... },
          { definition: "Someone who computes", ... }
        ]
      },
      {
        partOfSpeech: "verb",
        definitions: [ ... ]
      }
    ]
  }
}

4. Fetch Free Dictionary Data

Fallback for definitions: Request:
content.js:527
const fdResponse = await chrome.runtime.sendMessage({
  action: 'fetchFreeDictionary',
  term: term.toLowerCase(),
  language: currentLanguage
});
Response:
{
  success: true,
  data: [
    {
      word: "computer",
      phonetic: "/kəmˈpjuː.tə(ɹ)/",
      phonetics: [
        { text: "/kəmˈpjuː.tə(ɹ)/", audio: "https://..." }
      ],
      meanings: [
        {
          partOfSpeech: "noun",
          definitions: [
            { definition: "A programmable electronic device...", ... }
          ]
        }
      ],
      origin: "From compute + -er"
    }
  ]
}

5. Search Wikidata Entity

First step for Facts tab: Request:
content.js:749
const searchResponse = await chrome.runtime.sendMessage({
  action: 'searchWikidata',
  term: term,
  language: currentLanguage
});
Response:
{
  success: true,
  data: {
    id: "Q42",
    label: "Douglas Adams",
    description: "English science fiction writer and humourist"
  }
}

6. Fetch Wikidata Entity Details

Request:
content.js:766
const entityResponse = await chrome.runtime.sendMessage({
  action: 'fetchWikidataEntity',
  entityId: entityId,
  language: currentLanguage
});
Response:
{
  success: true,
  data: {
    label: "Douglas Adams",
    description: "English science fiction writer and humourist",
    facts: [
      { label: "Born", value: "11 March 1952" },
      { label: "Died", value: "11 May 2001" },
      { label: "Country", value: "United Kingdom" },
      { label: "Occupation", value: "novelist, screenwriter, essayist" },
      { label: "Notable work", value: "The Hitchhiker's Guide to the Galaxy" }
    ]
  }
}

7. Call OpenAI

Handles AI chat, explanations, and translations: Request (Explain Simply):
content.js:1207
const response = await chrome.runtime.sendMessage({
  action: 'callOpenAI',
  messages: [
    { role: 'system', content: 'You are a helpful assistant that explains topics in simple, plain language. Respond in English. Keep your explanation to 2-3 short sentences that anyone can understand.' },
    { role: 'user', content: 'Explain "Quantum Computing" in simple, easy-to-understand language.' }
  ],
  language: currentLanguage
});
Request (Chat):
content.js:1292
const systemPrompt = buildSystemPrompt('chat');
const messages = [{ role: 'system', content: systemPrompt }];

// Add conversation history
for (const msg of aiConversation) {
  if (msg.role === 'user' || msg.role === 'assistant') {
    messages.push({ role: msg.role, content: msg.content });
  }
}

const response = await chrome.runtime.sendMessage({
  action: 'callOpenAI',
  messages: messages,
  language: currentLanguage
});
Request (Translate):
content.js:1133
const systemPrompt = 'You are a professional translator. Translate the provided text to Arabic. Output ONLY the translation.';

const response = await chrome.runtime.sendMessage({
  action: 'callOpenAI',
  messages: [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: currentSearchTerm }
  ],
  language: currentLanguage,
  maxTokens: 1000
});
Response:
{
  success: true,
  data: "Quantum computing uses quantum mechanics to perform calculations much faster than traditional computers. It works with quantum bits (qubits) that can exist in multiple states at once, allowing parallel processing of information."
}
Error Response:
{
  success: false,
  error: "INVALID_API_KEY" // Or "RATE_LIMITED", "INSUFFICIENT_QUOTA", etc.
}

8. Check OpenAI Key

Verifies if API key is configured: Request:
content.js:379
const response = await chrome.runtime.sendMessage({ 
  action: 'checkOpenAIKey' 
});
Response:
{
  hasKey: true // or false
}
This is the only synchronous handler—it uses chrome.storage.local.get() with a callback that calls sendResponse() immediately.

Async Response Pattern

All async handlers follow this pattern:
background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'someAction') {
    // Start async operation
    fetchSomeData(message.params)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    
    return true; // CRITICAL: Keeps message channel open
  }
});
Why return true? Without it, the message channel closes immediately, and sendResponse() won’t work. This is a Chrome API requirement for async responses.

Error Handling Pattern

Content Script Side

Consistent try-catch with user-friendly error messages:
content.js:409
try {
  let response = await chrome.runtime.sendMessage({
    action: 'fetchWikipedia',
    term: cleanedTerm,
    language: currentLanguage
  });

  if (!response.success) {
    // Handle specific errors
    if (response.error?.includes('404')) {
      errorMsg = 'No Wikipedia article found';
    } else if (response.error?.includes('429')) {
      errorMsg = 'Too many requests. Please try again in a moment.';
    } else {
      errorMsg = 'Unable to fetch Wikipedia data. Please try again.';
    }
    showTabError(contentArea, errorMsg);
    return;
  }

  // Success: render data
  const data = response.data;
  renderSummaryContent(data, contentArea);

} catch (error) {
  console.error('Wikipedia fetch error:', error);
  showTabError(contentArea, 'Unable to connect. Please check your internet connection.');
}

Background Worker Side

Throws errors with HTTP status codes:
background.js:84
async function fetchWikipediaData(term, language) {
  const wikiDomain = language === 'ar' ? 'ar.wikipedia.org' : 'en.wikipedia.org';
  const url = `https://${wikiDomain}/api/rest_v1/page/summary/${encodeURIComponent(term)}`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return await response.json();
}

OpenAI Error Mapping

Special handling for AI errors:
background.js:274
if (!response.ok) {
  const status = response.status;
  
  if (status === 401) {
    throw new Error('INVALID_API_KEY');
  } else if (status === 429) {
    throw new Error('RATE_LIMITED');
  } else if (status === 402) {
    throw new Error('INSUFFICIENT_QUOTA');
  }
  
  throw new Error(`HTTP ${status}`);
}
Content script translates these:
content.js:1336
function handleAIError(errorMsg, chatArea) {
  let displayMsg;

  if (errorMsg === 'NO_API_KEY') {
    displayMsg = 'No API key set. Add one in extension settings.';
  } else if (errorMsg === 'INVALID_API_KEY') {
    displayMsg = 'Invalid API key. Please check your key in settings.';
  } else if (errorMsg === 'RATE_LIMITED') {
    displayMsg = 'Too many requests. Please wait a moment.';
  } else if (errorMsg === 'INSUFFICIENT_QUOTA') {
    displayMsg = 'Insufficient OpenAI quota. Please check your account.';
  } else {
    displayMsg = 'An error occurred. Please try again.';
  }

  chatArea.appendChild(createChatMessage('system', displayMsg));
}

Bidirectional Communication

The popup can also send messages to content scripts: From popup.html/popup.js:
// Toggle extension on/off
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  chrome.tabs.sendMessage(tabs[0].id, {
    action: 'toggleExtension',
    enabled: false
  });
});
Received in content script:
content.js:59
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'toggleExtension') {
    isEnabled = message.enabled;
    if (!isEnabled) {
      removeButton();
      removeTooltip();
    }
  } else if (message.action === 'changeLanguage') {
    preferredLanguage = message.language;
    cache.clear();
  }
});
chrome.tabs.sendMessage() targets a specific tab, while chrome.runtime.sendMessage() goes to the background worker.

Performance Considerations

Parallel Requests

Content script can send multiple messages simultaneously:
// These run in parallel
const [summaryPromise, factsPromise] = await Promise.all([
  chrome.runtime.sendMessage({ action: 'fetchWikipedia', term, language }),
  chrome.runtime.sendMessage({ action: 'searchWikidata', term, language })
]);

Caching to Reduce Messages

Content script caches API responses:
content.js:328
const cached = getFromCache(cleanedTerm, tabId);
if (cached) {
  renderTabContent(tabId, cached, contentArea);
  return; // Skip message passing
}

// Only send message if cache miss
chrome.runtime.sendMessage({ action: 'fetch...', ... });
Caching reduces message overhead and API calls, especially when users switch tabs frequently.

Security Implications

Content Script Isolation

Content scripts cannot:
  • Access chrome.storage.local where the OpenAI key is stored
  • Make direct API calls to Wikipedia/OpenAI (CORS blocked)
  • Execute privileged extension APIs

Message Validation

Background worker should validate message sources:
// Check sender.id matches extension ID
if (sender.id !== chrome.runtime.id) {
  return; // Ignore external messages
}
Knowledge Tooltip doesn’t currently implement sender validation, but it’s a best practice for production extensions.

Debugging Tips

Console Logging

Content script: Logs appear in page DevTools console
console.log('[Content] Sending message:', message);
Background worker: Logs appear in extension DevTools (chrome://extensions → Inspect)
console.log('[Background] Received message:', message);

Message Tracing

Add logging to both sides:
// Content script
const response = await chrome.runtime.sendMessage(message);
console.log('[Content] Response:', response);

// Background worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('[Background] Message:', message);
  // ... handler
  console.log('[Background] Sending response:', responseData);
  sendResponse(responseData);
});

Common Issues

  1. “The message port closed before a response was received”
    • Cause: Forgot return true in async handler
    • Fix: Add return true; after calling .then() or .catch()
  2. “Uncaught TypeError: sendResponse is not a function”
    • Cause: Calling sendResponse() after channel closed
    • Fix: Ensure return true is present
  3. No response received
    • Cause: Handler doesn’t match message.action
    • Fix: Check action string spelling

Message Passing Best Practices

  1. Consistent format: Always use { success, data/error } responses
  2. Action namespacing: Use descriptive action names (e.g., fetchWikipedia not fetch)
  3. Error handling: Catch all errors and return structured error responses
  4. Return true: Always return true from async handlers
  5. Validate inputs: Check message parameters before processing
  6. Log judiciously: Add console.log for debugging but remove in production

Next Steps

Content Script

How content script uses message passing

Background Worker

Message handler implementations

Architecture Overview

High-level system design

Build docs developers (and LLMs) love