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
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 :
const response = await chrome . runtime . sendMessage ({
action: 'fetchWikipedia' ,
term: 'Quantum Computing' ,
language: 'en'
});
Handler :
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 :
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 :
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 :
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 :
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 :
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) :
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) :
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) :
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 :
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:
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:
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:
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:
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:
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 :
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.
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:
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
“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()
“Uncaught TypeError: sendResponse is not a function”
Cause : Calling sendResponse() after channel closed
Fix : Ensure return true is present
No response received
Cause : Handler doesn’t match message.action
Fix : Check action string spelling
Message Passing Best Practices
Consistent format : Always use { success, data/error } responses
Action namespacing : Use descriptive action names (e.g., fetchWikipedia not fetch)
Error handling : Catch all errors and return structured error responses
Return true : Always return true from async handlers
Validate inputs : Check message parameters before processing
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