How to avoid "Extension context invalidated" errors when messaging AFTER an Extension update?

Google Chrome-Extension

Google Chrome-Extension Problem Overview


I am trying to create a smooth experience for users of my Chrome Extension after I release updates.

I reinject my content script on an update of the app, and my functionality works even if the user continues to use my Extension on a page that has NOT been refreshed after the Extension update. Refreshing the page is ideal, but I don't want to have to force that on my users.

However, I get the following errors in my content script console (the page the content script is inserted into) AFTER the Extension updates but BEFORE the page is refreshed when my code attempts to message from the content script/page to my background page:

> Uncaught Error: Extension context invalidated.

Is there a way to 'rebuild' the connection? I've tried a long-lived port and regular messaging - updating the Extension has the same effect.

Any ideas/direction appreciated.

Here is my simple messaging code (in the content script) that throws the error...but IS able to communicate with the background script:

chrome.runtime.sendMessage({ msg: "recordFeedback", obj: commentObj, source: source}, function(response){
  console.log('Sent to DB...response was: ', response, response.recordFeedbackResponse);
});

Google Chrome-Extension Solutions


Solution 1 - Google Chrome-Extension

When an extension is unloaded, the existing content scripts lose their connection to the rest of the extension—i.e. ports close, and they would be unable to use runtime.sendMessage()—but the content scripts themselves still continue to work, as they're already injected into their pages.

It's the same when an extension is reloaded: those existing content scripts will still be there, but unable to send messages; attempts to do so cause errors like the one you're encountering. Further, as it's common for extensions to inject their content scripts into existing tabs when they are loaded (on Chrome—Firefox does that automatically), you'll end up with more than one copy of the content script running in a given tab: the original, now-disconnected one, and the current, connected one.

Problems can arise if either: (1) your original content script is still trying to communicate with the rest of the extension or (2) your original content script does things like modify the DOM, as you could end up with those changes done more than once.

That's probably why you're still getting the error even after re-injecting your content scripts: the error is coming from the original content script that's still there, but disconnected.

This has paraphrased some helpful background information that I found when previously researching this problem. Note that Firefox automatically unloads content scripts (more on that later).

Solutions

If you are not automatically re-injecting your content scripts, then you can try to get the existing script to reconnect on upgrade, as per wOxxOm's comment above.

Here's some further info and advice, particularly useful if you inject new content scripts and want to disable the original, disconnected, one when a new one is injected.

Regardless of whether you would like to try to reconnect using the existing content script, or stop the original content script from making further changes and inject a new one, you need to detect that an unload has occurred. You can use the port's disconnect handler to catch when your extension is unloaded. If your extension only uses simple one-time message passing, and not ports, then that code is as simple as running the following when your content script starts up (I first saw this technique in some code from lydell).

browser.runtime.connect().onDisconnect.addListener(function() {
	// clean up when content script gets disconnected
})

On Firefox, the port's disconnect is not fired, because the content scripts are removed before they would receive it. This has the advantage that this detection isn't necessary (nor is manual content script injection, as content scripts are injected automatically too), but it does mean that there's no opportunity to reset any changes made to the DOM by your content script when it was running.

Solution 2 - Google Chrome-Extension

if(typeof chrome.app.isInstalled!=='undefined'){
   chrome.runtime.sendMessage()
}

Solution 3 - Google Chrome-Extension

It took me hours of reading docs, SO, chromium bug reports, to finally get all bugs fixed related to this "Reconnect to runtime after extension reloaded" issue.

This solution reinstalls the content script after the extension has been updated/reloaded and ensures that the old content script does not communicate with the invalidated runtime anymore.

Here is the whole relevant code:

manifest.json

All URLs from content_scripts.matches must be also included in the permissions array. That is required for the chrome.tabs.executeScript() to work.
Actually, you may drop the content_scripts object (as its only reason is to auto-inject content scripts) and handle everything in the background.js yourself (see docs: "Inject Scripts"). But I kept and used it for better overview and "compatibility" reasons.

{
    "permissions": [
        "tabs",
        "http://example.org",
    ],
    "background": {
        "scripts": ["background.js"],
        "persistent": false
    },
    "content_scripts": [
        {
            "matches": ["http://example.org/*"],
            "js": ["contentScript.js"]
        }
    ]
}

Note that content_scripts is an array as well as content_scripts[n]['js'].

background.js

const manifest = chrome.runtime.getManifest();

function installContentScript() {
  // iterate over all content_script definitions from manifest
  // and install all their js files to the corresponding hosts.
  let contentScripts = manifest.content_scripts;
  for (let i = 0; i < contentScripts.length; i++) {
    let contScript = contentScripts[i];
    chrome.tabs.query({ url: contScript.matches }, function(foundTabs) {
      for (let j = 0; j < foundTabs.length; j++) {
        let javaScripts = contScript.js;
        for (let k = 0; k < javaScripts.length; k++) {
          chrome.tabs.executeScript(foundTabs[j].id, {
            file: javaScripts[k]
          });          
        }
      }
    });
  }
}

chrome.runtime.onInstalled.addListener(installContentScript);

contentScript.js

var chromeRuntimePort = chrome.runtime.connect();
chromeRuntimePort.onDisconnect.addListener(() => {
  chromeRuntimePort = undefined;
});

// when using the port, always check if valid/connected
function postToPort(msg) {
  if (chromeRuntimePort) {
    chromeRuntimePort.postMessage(msg);
  }
}

// or
chromeRuntimePort?.postMessage('Hey, finally no errors');

Solution 4 - Google Chrome-Extension

I would suggest accessing chrome.runtime.id as a way to tell if the extension context was invalidated or not.

// use null-safe operator since chrome.runtime
// is lazy inited and might return undefined
//
if (chrome.runtime?.id) {
   
}

Solution 5 - Google Chrome-Extension

Try this in your background script. Many of the old methods have been deprecated now, so I have refactored the code. For my use I'm only installing single content_script file. If need you can iterate over chrome.runtime.getManifest().content_scripts array to get all .js files.

chrome.runtime.onInstalled.addListener(installScript);

function installScript(details){
    // console.log('Installing content script in all tabs.');
    let params = {
        currentWindow: true
    };
    chrome.tabs.query(params, function gotTabs(tabs){
        let contentjsFile = chrome.runtime.getManifest().content_scripts[0].js[0];
        for (let index = 0; index < tabs.length; index++) {
            chrome.tabs.executeScript(tabs[index].id, {
                file: contentjsFile
            },
            result => {
                const lastErr = chrome.runtime.lastError;
                if (lastErr) {
                    console.error('tab: ' + tabs[index].id + ' lastError: ' + JSON.stringify(lastErr));
                }
            })
        }
    });    
}

Solution 6 - Google Chrome-Extension

The top answer didn't work for me in Chrome. Instead of running when the extension is reloaded, it runs every page, even on refreshes.

Here is what I've decided to implement (tested in Chrome and Firefox). This checks if an error (the english string) is caused from extension being reloaded in development. If this is true then it disables functions from executing error-causing code across the extension via a global var. It logs all other errors.

function isReloadExtErr(err) {
	if (err.toString().includes("Extension context invalidated")) {
		console.log("Extension has been reloaded, set global var");
		extensionActive = false;
		return true;
	}
	return false;
}

And then inside functions which would cause errors after a refresh (those that use browser APIs)

function someFunction() {
	try { 
		if (!extensionActive) return;
		/* code */ 
	} catch (err) {
		if (!isReloadExtErr(err)) console.error(err);
	}
}

Solution 7 - Google Chrome-Extension

close the browser tab and start over. Worked for me.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Question11teenthView Question on Stackoverflow
Solution 1 - Google Chrome-ExtensionmatatkView Answer on Stackoverflow
Solution 2 - Google Chrome-Extensionw2cView Answer on Stackoverflow
Solution 3 - Google Chrome-ExtensionMartin SchneiderView Answer on Stackoverflow
Solution 4 - Google Chrome-ExtensionresuView Answer on Stackoverflow
Solution 5 - Google Chrome-ExtensionPrateek JhaView Answer on Stackoverflow
Solution 6 - Google Chrome-Extensionow3nView Answer on Stackoverflow
Solution 7 - Google Chrome-ExtensionjimView Answer on Stackoverflow