Sending Message from a Background Script to a Content Script, Then to a Injected Script

Sending message from a background script to a content script, then to a injected script

Your script doesn't work because of how content scripts are injected.

Problem

When you (re)load your extension, contrary to what some people expect, Chrome will not inject content scripts into existing tabs that match patterns from the manifest. Only after the extension is loaded, any navigation will check the URL for matching and will inject the code.

So, the timeline:

  1. You open some tabs. No content scripts there1.
  2. You load your extension. Its top level code gets executed: it tries to pass a message to the current tab.
  3. Since there can be no listener there yet, it fails. (Which is probably the chrome://extensions/ page and you can't inject there anyway)
  4. If, afterwards, you try to navigate/open a new tab, the listener gets injected, but your top level code no longer gets executed.

1 - This also happens if you reload your extension. If there was a content script injected, it continues to handle its events / doesn't get unloaded, but can no longer communicate with the extension. (for details, see addendum at the end)

Solutions

Solution 1: you can first ask the tab you're sending a message to whether it's ready, and upon silence inject the script programmatically. Consider:

// Background
function ensureSendMessage(tabId, message, callback){
chrome.tabs.sendMessage(tabId, {ping: true}, function(response){
if(response && response.pong) { // Content script ready
chrome.tabs.sendMessage(tabId, message, callback);
} else { // No listener on the other end
chrome.tabs.executeScript(tabId, {file: "content_script.js"}, function(){
if(chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
throw Error("Unable to inject script into tab " + tabId);
}
// OK, now it's injected and ready
chrome.tabs.sendMessage(tabId, message, callback);
});
}
});
}

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
ensureSendMessage(tabs[0].id, {greeting: "hello"});
});

and

// Content script
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(request.ping) { sendResponse({pong: true}); return; }
/* Content script action */
});

Solution 2: always inject a script, but make sure it only executes once.

// Background
function ensureSendMessage(tabId, message, callback){
chrome.tabs.executeScript(tabId, {file: "content_script.js"}, function(){
if(chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
throw Error("Unable to inject script into tab " + tabId);
}
// OK, now it's injected and ready
chrome.tabs.sendMessage(tabId, message, callback);
});
}

and

// Content script
var injected;

if(!injected){
injected = true;
/* your toplevel code */
}

This is simpler, but has complications on extension reload. After an extension is reloaded, the old script is still there1 but it's not "your" context anymore - so injected will be undefined. Beware of side effects of potentially executing your script twice.


Solution 3: just indiscriminately inject your content script(s) on initialization. This is only safe to do if it's safe to run the same content script twice, or run it after the page is fully loaded.

chrome.tabs.query({}, function(tabs) {
for(var i in tabs) {
// Filter by url if needed; that would require "tabs" permission
// Note that injection will simply fail for tabs that you don't have permissions for
chrome.tabs.executeScript(tabs[i].id, {file: "content_script.js"}, function() {
// Now you can use normal messaging
});
}
});

I also suspect that you want it to run on some action, and not on extension load. For example, you can employ a Browser Action and wrap your code in a chrome.browserAction.onClicked listener.


Addendum on orphaned content scripts

When an extension gets reloaded, one would expect Chrome to clean up all content scripts. But apparently this is not the case; content scripts' listeners are not disabled. However, any messaging with parent extension will fail. This should probably be considered a bug and may at some point be fixed. I'm going to call this state "orphaned"

This is not a problem in either of two cases:

  1. Content script has no listeners for events on the page (e.g. only executes once, or only listens to messages from background)
  2. Content script does not do anything with the page, and only messages the background about events.

However, if that's not the case, you've got a problem: the content script might be doing something but failing or interfering with another, non-orphaned instance of itself.

A solution to this would be:

  1. Keep track of all event listeners that can be triggered by the page
  2. Before acting on those events, send a "heartbeat" message to background.
    3a. If the background responds, we're good and should execute the action.
    3b. If the message passing fails, we're orphaned and should desist; ignore the event and deregister all listeners.

Code, content script:

function heartbeat(success, failure) {
chrome.runtime.sendMessage({heartbeat: true}, function(reply){
if(chrome.runtime.lastError){
failure();
} else {
success();
}
});
}

function handler() {
heartbeat(
function(){ // hearbeat success
/* Do stuff */
},
function(){ // hearbeat failure
someEvent.removeListener(handler);
console.log("Goodbye, cruel world!");
}
);
}
someEvent.addListener(handler);

Background script:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(request.heartbeat) { sendResponse(request); return; }
/* ... */
});

Chrome extension, best way to send messages from injected script to background

Your injected.js is running in a script DOM element so it's just an unprivileged web page script. To use chrome.runtime.sendMessage directly from a web page script you would have to declare the allowed URL patterns in externally_connectable but you can't allow all URLs indiscriminately so I would use your approach, although with a CustomEvent (and a random event name most likely) instead of message which can be intercepted by the page scripts or other extensions - it's not security that I'm concerned with here, but rather a possibility of breaking some site that assumes the message data is in a certain format (e.g. string), not compatible with the one you use (e.g. object).

You can also use chrome.debugger API to attach to the tab and intercept the JSON response in your background script, but that will show a notification above the tab about it being debugged.

Anyway, unless you see your extension slowing down the page (in devtools profiler or by manually measuring the time spent in your code), there's no need to worry.

FWIW, in Firefox you can read the response directly via browser.webRequest.filterResponseData.



Related Topics



Leave a reply



Submit