9

I send messages from the injected content scripts back to my background script in my Chrome Extension as such:

chrome.runtime.sendMessage({action: "myResult"});

This works fine, until I reload my extension (by going to Settings -> Extensions -> "Reload (Ctrl+R)" for my extension.)

In turn when my background script starts up it repeatedly calls chrome.tabs.executeScript for all open tabs to programmatically re-inject my content script (as I showed in this question.)

But after I do that, if I call that first sendMessage line from my content script, it throws this exception:

Error: Error connecting to extension my_extension_id

Any idea why this happens?

Community
  • 1
  • 1
c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • On-topic, ensure two things: 1) you're not getting this line from a previous instance of the content script, 2) you're not injecting the scripts before you've registered an `onMessage` listener. – Xan Sep 15 '14 at 06:33
  • @Xan: Yes. Isn't it what this site if for? I obviously put an effort to research and ask only when I run into a dead-end. – c00000fd Sep 15 '14 at 08:29
  • So going back to your on-topic comments. How do I ensure #1? – c00000fd Sep 15 '14 at 08:30

1 Answers1

16

When the extension runtime is reloaded, which happens in any of the following cases

  • You've called chrome.runtime.reload().
  • You've clicked on Reload extension at chrome://extensions/.
  • The extension was updated.

then the most extension API methods in the content script cease to work (including chrome.runtime.sendMessage which causes the error in the question). There are two ways to work around this problem.

Option 1: Fall back to contentscript-only functionality

If your extension can function perfectly without a background page, then this could be an acceptable solution. E.g. if your content script does nothing else besides modifying the DOM and/or performing cross-origin requests.

I'm using the following snippet in one of my extensions to detect whether the runtime is still valid before invoking any Chrome extension API from my content script.

// It turns out that getManifest() returns undefined when the runtime has been
// reload through chrome.runtime.reload() or after an update.
function isValidChromeRuntime() {
    // It turns out that chrome.runtime.getManifest() returns undefined when the
    // runtime has been reloaded.
    // Note: If this detection method ever fails, try to send a message using
    // chrome.runtime.sendMessage. It will throw an error upon failure.
    return chrome.runtime && !!chrome.runtime.getManifest();
}

// E.g.
if (isValidChromeRuntime()) {
    chrome.runtime.sendMessage( ... );
} else {
    // Fall back to contentscript-only behavior
}

Option 2: Unload the previous content script on content script insertion

When a connection with the background page is important to your content script, then you have to implement a proper unloading routine, and set up some events to unload the previous content script when the content script is inserted back via chrome.tabs.executeScript.

// Content script
function main() {
    // Set up content script
}

function destructor() {
    // Destruction is needed only once
    document.removeEventListener(destructionEvent, destructor);
    // Tear down content script: Unbind events, clear timers, restore DOM, etc.
}

var destructionEvent = 'destructmyextension_' + chrome.runtime.id;
// Unload previous content script if needed
document.dispatchEvent(new CustomEvent(destructionEvent));
document.addEventListener(destructionEvent, destructor);
main();

Note that any page that knows the name of the event can trigger destruction of your content script. This is unavoidable, because after the extension runtime is destroyed, there are no proper ways to securely communicate with the extension any more.

Rob W
  • 341,306
  • 83
  • 791
  • 678
  • I appreciate you taking time to explain this! One question about your option 2 though. The reason I begin re-injecting content scripts after a reload is because I can no longer communicate with them from the background script. But if so, how would I let them know that they have to unload? On the other hand, neither content scripts nor background script itself have any means of knowing that the extension is about to be unloaded to do what you explained. Wouldn't this scenario qualify for a _catch 22_? – c00000fd Sep 15 '14 at 09:00
  • 2
    @c00000fd I'm using a custom DOM event (see last four lines before `main();` at the end of my snippet) to manage unloading. First I trigger the custom event to instruct any listeners (presumably a previous content script) to clean up itself. Then I bind a new event listener that listens to the event. Finally I set up the content script by calling the `main()` function. – Rob W Sep 15 '14 at 09:02
  • Yes, I can see that. So DOM events will work, hah. That's a good thought, thanks! – c00000fd Sep 15 '14 at 09:04
  • +1 for sending a DOM event, never thought about that. – Xan Sep 15 '14 at 09:45
  • @c00000fd DOM event work because the content script really just continues executing, only cut off the parent background page. – Xan Sep 15 '14 at 09:46
  • @RobW Question about option 1. Would `chrome.storage` fail in this case? Then it's not enough not to employ background pages. – Xan Sep 15 '14 at 09:47
  • @Xan Most Chrome extension APIs will fail (in an inconsistent way). In the case of `chrome.storage`, the callback will not be called. – Rob W Sep 15 '14 at 09:49
  • @RobW: So if `"Most Chrome extension APIs will fail (in an inconsistent way)"` your option 1 is really a no-go. What real-world app would be able to function like that? – c00000fd Sep 15 '14 at 10:03
  • @RobW This answer inspired me to add a feature request: https://code.google.com/p/chromium/issues/detail?id=414213 - it would be great if you could triage that. – Xan Sep 15 '14 at 10:08
  • @Xan: It actually sounds like a bug to me. I've been coding for a while and it's the first time I hear about the need for a destructor in JS. The language was not designed for it. – c00000fd Sep 15 '14 at 10:12
  • Here's an example. Say, the "unloading" part. Say, OK, I manage to dispatch that DOM event to a content script and it removes all DOM event listeners, stops timers, etc. how will my background script know that the content script is actually unloaded before re-injecting it? I'm sure it won't happen immediately until the garbage collector gets to it. – c00000fd Sep 15 '14 at 10:12
  • @c00000fd Why would you care if it's unloaded or not? The important part is that no code from it will be called anymore. That's the point of this "destructor". – Xan Sep 15 '14 at 10:13
  • @Xan: Because it created a memory leak if my background.js starts injecting them without verifying that the old script is gone. – c00000fd Sep 15 '14 at 10:15
  • @c00000fd Note I said *inconsistent* as in API-inconsistent (some methods throw an error (like `chrome.runtime.sendMessage()`), some return `undefined` (like `chrome.runtime.getManifest()`) and some do not invoke the callback at all (like `chrome.storage.local.get`)). The failures are deterministic and predictable, and JavaScript runs on a synchronous thread, so the option 1 is reliable (that is, if you are willing to rely on undocumented characteristics). – Rob W Sep 15 '14 at 10:31
  • @c00000fd The point of invoking `dispatchEvent` before `main` is that the `destructor` function synchronously undos the changes that are added by the content script during its lifetime. If the "destructor" function is properly written, the old "destructed" content script context will be idle for the remainder of its life, giving the illusion that the content script world is completely destructed. – Rob W Sep 15 '14 at 10:34
  • @RobW: Yeah, I hear you. That's the issue too "undocumented characteristics". Quick question to you. If I call `chrome.tabs.executeScript` on the same tabID twice, will it create two running copies of the background script? – c00000fd Sep 15 '14 at 10:34
  • @c00000fd No, never. There will always be one background script. If you repeatedly call `chrome.tabs.executeScript`, then there will only be one content script world. Note that *usually* there is only one content script world; the only situation where multiple content script worlds exist is when the extension runtime is reloaded, as explained at the top of this answer. – Rob W Sep 15 '14 at 10:36
  • @RobW: The reason I'm asking is if I go with your option 2, i.e. unbinding events first when content.js just starts up again after a call to `chrome.tabs.executeScript` from the background.js, there's none of my DOM objects exist in it at the time. It's almost like the content.js is just loading for the first time. – c00000fd Sep 15 '14 at 10:39
  • And yes, I'm calling it after the extension is reloaded. That's the whole point of my issue here. – c00000fd Sep 15 '14 at 10:41
  • @RobW: OK, I take my last statement back. Yes, DOM objects exist in the old content.js, but it's a **pain unloading** them. Plus, there's evidently more to it because copies of my injected content.js seem to multiply like rabbits every time extension is reloaded and I call `chrome.tabs.executeScript`. – c00000fd Sep 15 '14 at 10:59
  • Final thoughts for whoever else also deals with this. (Sorry, it got too long.) The option 2 suggested above does work once you manage to remove all event listeners and timers, etc just like he showed above. PS. I'm not sure though how one would do it if you use any 3rd party library like jQuery. Luckily I didn't need it in my content script. Thanks for all your help! – c00000fd Sep 16 '14 at 01:09
  • I got option 2 working with a variant: I made a really small script called `bg_comm.js` that forwards all information via the DOM (`new CustomEvent...` and `window.addEventListener`) to a content_script that is only loaded once. This way, I don't have to worry about cleaning up after the content script, just `bg_comm.js` – theicfire Jun 28 '20 at 04:43
  • Also, I just listened for the `port.onDisconnect` event to decide when to deconstruct my content script. This is simpler and I don't think has downsides. – theicfire Jun 28 '20 at 04:53
  • for option 1, should use `return chrome.runtime && !!chrome.runtime.id;` now (2020-10) – Walty Yeung Oct 15 '20 at 03:12