Messaging APIs allow you to communicate between different scripts running in contexts associated with your extension. This includes communication between your service worker, chrome-extension://pages and content scripts. For example, an RSS reader extension might use content scripts to detect the presence of an RSS feed on a page, then notify the service worker to update the action icon for that page.
There are two message passing APIs: one for one-time requests, and a more complex one for long-lived connections that allow multiple messages to be sent.
For information about sending messages between extensions, see the cross-extension messages section.
One-time requests
To send a single message to another part of your extension, and optionally get a
response, call runtime.sendMessage()
or tabs.sendMessage()
.
These methods let you send a one-time JSON-serializable message from a content script to the
extension, or from the extension to a content script. Both APIs return a Promise
which resolves to the response provided by a recipient.
Sending a request from a content script looks like this:
content-script.js:
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
Responses
To listen for a message, use the chrome.runtime.onMessage
event:
// Event listener
function handleMessages(message, sender, sendResponse) {
fetch(message.url)
.then((response) => sendResponse({statusCode: response.status}))
// Since `fetch` is asynchronous, must return an explicit `true`
return true;
}
chrome.runtime.onMessage.addListener(handleMessages);
// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage({
url: 'https://example.com'
});
When the event listener is called, a sendResponse
function is passed as the
third parameter. This is a function that can be called to provide a response. By
default, the sendResponse
callback must be called synchronously. If you want
to do asynchronous work to get the value passed to sendResponse
, you must
return a literal true
(not just a truthy value) from the event listener. Doing
so will keep the message channel open to the other end until sendResponse
is
called.
If you call sendResponse
without any parameters, null
is sent as a response.
If multiple pages are listening for onMessage
events, only the first to call sendResponse()
for
a particular event will succeed in sending the response. All other responses to that event will be
ignored.
Long-lived connections
To create a reusable long-lived message passing channel, call:
runtime.connect()
to pass messages from a content script to an extension pagetabs.connect()
to pass messages from an extension page to a content script.
You can name your channel by passing an options parameter with a name
key to
distinguish between different types of connections:
const port = chrome.runtime.connect({name: "example"});
One potential use case for a long-lived connection is an automatic form-filling extension. The content script might open a channel to the extension page for a specific login, and send a message to the extension for each input element on the page to request the form data to fill in. The shared connection allows the extension to share state between extension components.
When establishing a connection, each end is assigned a runtime.Port
object for
sending and receiving messages through that connection.
Use the following code to open a channel from a content script, and send and listen for messages:
content-script.js:
const port = chrome.runtime.connect({name: "knockknock"});
port.onMessage.addListener(function(msg) {
if (msg.question === "Who's there?") {
port.postMessage({answer: "Madame"});
} else if (msg.question === "Madame who?") {
port.postMessage({answer: "Madame... Bovary"});
}
});
port.postMessage({joke: "Knock knock"});
To send a request from the extension to a content script, replace the call to runtime.connect()
in the previous example with tabs.connect()
.
To handle incoming connections for either a content script or an extension page, set
up a runtime.onConnect
event listener. When another part of your
extension calls connect()
, it activates this event and the runtime.Port
object. The code for responding to incoming connections looks like this:
service-worker.js:
chrome.runtime.onConnect.addListener(function(port) {
if (port.name !== "knockknock") {
return;
}
port.onMessage.addListener(function(msg) {
if (msg.joke === "Knock knock") {
port.postMessage({question: "Who's there?"});
} else if (msg.answer === "Madame") {
port.postMessage({question: "Madame who?"});
} else if (msg.answer === "Madame... Bovary") {
port.postMessage({question: "I don't get it."});
}
});
});
Serialization
In Chrome, the message passing APIs use JSON serialization. This means a message (and responses provided by recipients) can contain any valid JSON value (null, boolean, number, string, array, or object). Other values will be coerced into serializable values.
Notably, this is different to other browsers which implement the same APIs with the structured clone algorithm.
Port lifetime
Ports are designed as a two-way communication mechanism between different parts
of an extension. When part of an extension calls
tabs.connect()
, runtime.connect()
or
runtime.connectNative()
, it creates a
Port that can immediately send messages using
postMessage()
.
If there are multiple frames in a tab, calling tabs.connect()
invokes
the runtime.onConnect
event once for each frame in the tab. Similarly, if
runtime.connect()
is called, then the onConnect
event can fire once for every
frame in the extension process.
You might want to find out when a connection is closed, for example if you're maintaining separate
states for each open port. To do this, listen to the runtime.Port.onDisconnect
event. This
event fires when there are no valid ports at the other end of the channel, which can have any of the following causes:
- There are no listeners for
runtime.onConnect
at the other end. - The tab containing the port is unloaded (for example, if the tab is navigated).
- The frame where
connect()
was called has unloaded. - All frames that received the port (via
runtime.onConnect
) have unloaded. runtime.Port.disconnect()
is called by the other end. If aconnect()
call results in multiple ports at the receiver's end, anddisconnect()
is called on any of these ports, then theonDisconnect
event only fires at the sending port, not at the other ports.
Cross-extension messaging
In addition to sending messages between different components in your extension, you can use the messaging API to communicate with other extensions. This lets you expose a public API for other extensions to use.
To listen for incoming requests and connections from other extensions, use the
runtime.onMessageExternal
or runtime.onConnectExternal
methods. Here's an example of
each:
service-worker.js
// For a single request:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id !== allowlistedExtension) {
return; // don't allow this extension access
}
if (request.getTargetData) {
sendResponse({ targetData: targetData });
} else if (request.activateLasers) {
const success = activateLasers();
sendResponse({ activateLasers: success });
}
}
);
// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// See other examples for sample onMessage handlers.
});
});
To send a message to another extension, pass the ID of the extension you want to communicate with as follows:
service-worker.js
// The ID of the extension we want to talk to.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
}
);
// For a long-lived connection:
const port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
Send messages from web pages
Extensions can also receive and respond to messages from web pages. To send
messages from a web page to an extension, specify in your manifest.json
which
websites you want to allow messages from using the
"externally_connectable"
manifest key. For example:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
This exposes the messaging API to any page that matches the URL patterns you specify. The URL
pattern must contain at least a second-level domain; that is, hostname patterns such as "*",
"*.com", "*.co.uk", and "*.appspot.com" are not supported. You can use
<all_urls>
to access all domains.
Use the runtime.sendMessage()
or runtime.connect()
APIs to send
a message to a specific extension. For example:
webpage.js
// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';
// Check if extension is installed
if (chrome && chrome.runtime) {
// Make a request:
chrome.runtime.sendMessage(
editorExtensionId,
{
openUrlInEditor: url
},
(response) => {
if (!response.success) handleError(url);
}
);
}
From your extension, listen to messages from web pages using the
runtime.onMessageExternal
or runtime.onConnectExternal
APIs as in cross-extension messaging. Here's an example:
service-worker.js
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.url === blocklistedWebsite)
return; // don't allow this web page access
if (request.openUrlInEditor)
openUrl(request.openUrlInEditor);
});
It is not possible to send a message from an extension to a web page.
Native messaging
Extensions can exchange messages with native applications that are registered as a native messaging host. To learn more about this feature, see Native messaging.
Security considerations
Here are a few security considerations related to messaging.
Content scripts are less trustworthy
Content scripts are less trustworthy than the extension service worker. For example, a malicious web page might be able to compromise the rendering process that runs the content scripts. Assume that messages from a content script might have been crafted by an attacker and make sure to validate and sanitize all input. Assume any data sent to the content script might leak to the web page. Limit the scope of privileged actions that can be triggered by messages received from content scripts.
Cross-site scripting
Make sure to protect your scripts against cross-site scripting. When receiving data from an untrusted source such as user input, other websites through a content script, or an API, take care to avoid interpreting this as HTML or using it in a way which could allow unexpected code to run.
Use APIs that don't run scripts whenever possible:
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // JSON.parse doesn't evaluate the attacker's scripts. const resp = JSON.parse(response.farewell); });
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // innerText does not let the attacker inject HTML elements. document.getElementById("resp").innerText = response.farewell; });
Avoid using the following methods that make your extension vulnerable:
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be evaluating a malicious script! const resp = eval(`(${response.farewell})`); });
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be injecting a malicious script! document.getElementById("resp").innerHTML = response.farewell; });