0

I've been building my first Chrome extension. It has been fun however I've run into a problem I've yet to solve.

I get the error in the devtools extentions Developer mode:

Uncaught Error: Extension context invalidated.

I pretty sure that upon update of my extension and hard refresh on my test https:// page that the contentScript.js gets injected multiple times. The older injected scripts are still trying to the dom injections but the ports are not open so it throws the error?

I have tried the solutions in both of these threads as well going through google groups:

  1. Recursive "Extension context invalidated" error in console
  2. Extension context invalidated. Chrome Extension

I am using manifest v3.

Can you please suggest a way that I can update my code to protect against this error?

Here is my manifest:

{
    "name": "Focuser",
    "description": "Focuses on a page during a specified time frame",
    "version": "0.1.0",
    "manifest_version": 3,
    "background": {
        "service_worker": "background.js"
    },
    "permissions": [
        "storage",
        "scripting",
        "alarms"
    ],
    "host_permissions": [
        "<all_urls>"
    ]
    
}

My background script:

try {
     chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
         if ('undefined' !== typeof tab.url) {
            // skip urls like "chrome://" to avoid extension error
            if (tab.url?.startsWith("chrome://")) return undefined;
               if(changeInfo.status == 'complete') {

                 chrome.scripting.executeScript({
                     files: ['contentScript.js'],
                     target: {tabId: tab.id}
                 })   
               }
            }

     })

 } catch(e) {
    console.log(e)
}

My conentScript (with classes removed)

    /** 
 * Foozle Focuser - a class that injects a temporary DOM element 
 * into the site and then focus and click on it. 
 * 
 * The idea is that by maintaining the focus while running in the background a 
 * Chrome tab can stream radio stations and other media content. This could be used
 * for anything a user wants to click.
*/
class FoozleFocuser {
    /**
     * private {string} target
     * the dom element used as the target for the inject DOM 
     */
    #target = null;
    /**
     * private {string} injectElement 
     * the DOM element used as the target for the injected DOM 
     */
    #injectElement = '';
    /**
     * private {string} injectElementClassName
     * the CSS class to be added to the injected DOM element 
     */
    #injectElementClassName = '';

    /**
     * Constructor - set the target and injectElementClass - optionally no params passed
     * will set a target as document.body and the injectClassName will be foozle-focuser
     * @param {string} target - the passed target DOM element that will be used as the target for the injected DOM
     * @param {string} injectElementClassName - the CSS class to be added to the injected DOM element
     * @return Void
     */
    constructor(target=null, injectElementClassName = 'foozle-focuser') {
        this.#target = this.#getTarget(target);
        this.#injectElementClassName = injectElementClassName;
    }

    /**
     * private SetInjectedElement
     * Creates the injected DOM element with a class that will be used as a target for the focus
     * @param {string} domElement
     * @return Void
     */
    #setInjectedElement(domElement = 'div') {
        this.#injectElement = document.createElement(domElement);
        this.#injectElement.className = this.#injectElementClassName;
    }

    /**
     * private getTarget - queries the passed dom string. If null set the document.body as target
     * @param {string || null} target - The dom target element where the injection will be done
     * @return string - the target 
     */
    #getTarget(target=null) {

        if ( target == null ) {
            target = document.body;
        } else {
            target = document.querySelector(target);
            if ( target == null || 'undefined' == typeof target ) {
                target = document.body;    
            }
        }
        return target;
    }

    /**
     * private focus - appends, focuses on, and clicks the injected DOM Element
     * @return Void
     */
    #focus() {
        if (this.#target) {
            this.#target.appendChild(this.#injectElement);
            this.#target.focus();
            this.#target.click();
            let newDiv = document.querySelector('.' + this.#injectElementClassName)
            newDiv.parentNode.removeChild(newDiv);
        } 
    }
    /**
     * private run - runs the setup for the target and injected element and then focuses on the target
     * @return Void
     */
    run() {
        this.#setInjectedElement();
        this.#focus();
    }
}
class FoozleTypes {
    typeOf(value) {
        var s = typeof value;
        if (s === 'object') {
            if (value) {
                if (Object.prototype.toString.call(value) == '[object Array]') {
                    s = 'array';
                }
            } else {
                s = 'null';
            }
        }
        return s;
    }

    checkTypes(argList, typeList) {
        for (var i = 0; i < typeList.length; i++) {
            if (typeOf(argList[i]) !== typeList[i]) {
                throw 'wrong type: expecting ' + typeList[i] + ", found " + typeOf(argList[i]);
            }
        }
    }

}
class FoozleCounter {

    getStoredCount(key='focuserCount') {
        this.item = localStorage.getItem(key);
        if ( null == this.item || 'undefined' == this.item ) {
            localStorage.setItem(key, 0); 
        }
        this.item = parseInt(this.item)
        let count = this.item;
        count++;
        localStorage.setItem(key, count);
        return count;
    }
}

let FC = new FoozleCounter();
let FF = new FoozleFocuser();
if ('undefined' == typeof intervalId) {
    var intervalId = setInterval(() => {
        if (!chrome.runtime?.id) {
            // The extension was reloaded and this script is orphaned
            clearInterval(intervalId);
            return;
        }
        FF.run();
        // Get the updated count
        let count = FC.getStoredCount();
        // Store and report the count
        // @TODO - change the console log to the popup page
        chrome.storage.local.set({key: count}, function() {
            console.log('Count is set to ' + count);
        });

    }, 45000);
}

if('undefined' == typeof init ) {
    var init = () => {
        if (!chrome.runtime?.id) {
            // The extension was reloaded and this script is orphaned
            clearInterval(init);
            return;
        }
        FF.run();
        console.log('count', FC.getStoredCount())
    }
    init();
}
ServerStorm
  • 59
  • 1
  • 9
  • I tried to run your extension but gave up. Please create a minimal configuration that reproduces the phenomenon and post it. For example, you don't need an icon, do you? If you can reproduce without popups or backgrounds, you don't need that either. You may find answers along the way. – Norio Yamamoto Nov 03 '22 at 22:55
  • I did as you suggested and removed everthing not needed and included a complete base configuration. I am still getting Extension context invalidated. on the https;//chrome/extension page but no errors on the page where I am injecting content. – ServerStorm Nov 04 '22 at 15:06
  • Just refresh the page. The error you shared only occurs when you reload an extension. – Jridyard Nov 04 '22 at 22:46

1 Answers1

0

When the extension is auto-updated or reloaded on chrome://extensions page, its old content scripts are "orphaned" and can't use the chrome API anymore.

A universal solution is to re-inject the new content script in the above scenarios and send a DOM message to the orphaned content script so it can unregister its listeners and timers.

In your case, judging by the posted code, there's only setInterval, so the solution is simple:

var intervalId = setInterval(() => {
  if (!chrome.runtime?.id) {
    // The extension was reloaded and this script is orphaned
    clearInterval(intervalId);
    return;
  }
  //....................
  //....................
  //....................
}, 45000);

And of course you can re-inject the new content script if necessary.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • Hi wOxxOm, Thanks for your suggestions. Unfortunately I'm still getting the error. Minimal complete extension edited in the original post. Any futher things to try? – ServerStorm Nov 04 '22 at 15:10
  • I guess you should use `chrome.runtime?.id` everywhere. Also make sure you've reloaded all tabs after reloading the extension, because they still run the old content script that doesn't check the id properly. – wOxxOm Nov 04 '22 at 18:06
  • Thanks for your help! I was able to solve this using your suggestion plus I needed to do undefined checks as well. I have updated the code above to the working version. – ServerStorm Nov 08 '22 at 15:51