MV2 -> MV3 can't get injected javascript working

1,350 views
Skip to first unread message

peter....@gmail.com

unread,
Jan 12, 2022, 2:58:17 PM1/12/22
to Chromium Extensions
Previously in MV2, this code setup was working when I inserted the javascript string to a created script element using text property/field and appending it to the webpage. In this javascript, I access the chrome storage api to update things needed for when the tab next triggers the onChanged message. I don't need this part of the storage data in the background page, just the content script. In MV2 I did NOT need a separate file, additional manifest declarations, and chrome runtime message passing...

However, now with MV3, this does not work anymore. Upon research/reading it is my understanding that this is no longer allowed to be done. But, lots of users noted a workaround by using another .js file and having that new .js file in the manifest under "web_accessible_resources". I did this, and copied all the javascript string text as needed to the new .js file. I then set the previously created script element src property/field to the new file. The problem I run into is this new .js file has no access to the chrome api. I cannot use chrome.tabs to send a message, or chrome.local.storage, as the chrome.tabs and chrome.local are undefined.

I did notice I that the chrome variable has access to the runtime. A little more research mentions I can send messages from the webpage, using "externally_connected" in the manifest. I then added the chrome.runtime.sendMessage(...) call in the correct place in the .js file, so that I could receive the message in the background.js page and act accordingly. I added the chrome.runtime.onMessageExternal.addListener(...) function to the background.js file, with a simple console.log(...) call to verify it works, but alas, the background.js page never receives the message.

I did add my extensionid to the chrome.runtime.sendMessage call from the new .js page (the one that is appended/injected into the webpage). I also tried matching my extensionid and "*" for the externally_connected property of the manifest. I also added multiple variations of the webpage urls/domains for the matches section of the externally_connected (the same urls/domains that my content_scripts match on and work).

The injected javascript file does get an "undefined" for the sendMessage response, and triggers the "receiving end has closed" error. Which I know all to well because of my next message.

To preface, in the past 6 months I have mostly eliminated all the previous "sendMessage" and onMessage.* calls and replaced it with the port system. I also tried passing the port through to the injected javascript (stringifying the port with functions) and that does not work either.

These are the examples I've been following:
https://developer.chrome.com/docs/extensions/mv3/manifest/externally_connectable/
https://stackoverflow.com/questions/18124500/using-externally-connectable-to-send-data-from-www-to-chrome-extension

Snippets of my manifest (the example2 urls aren't needed, that's what the website was before example1 urls - these are the same as the matches in my content_scripts section):
"web_accessible_resources": [
    {
        "resources": [ "injected_scripts.js" ],
        "matches": [ "<all_urls>" ]
    }
],
"externally_connectable": {
        "ids": [
            "*"
        ],
        "matches": [
                "https://example1.com/*",
                "https://example2.2.com/*",
                "http://example2.2.com/*",
                "https://example1.com/exact/url"
        ]
}

Webpage javascript injection (which happens on above exact url):
var newJS = document.createElement('script');
newJS.src = chrome.runtime.getURL('/injected_scripts.js');
newJS.onload = function() {
    console.log('injected_scripts.js loaded...');
};
if(document.head) {
    document.head.appendChild(newJS);
} else if(document.documentElement) {
    document.documentElement.appendChild(newJS);
}

Injected javascript (inside one of the functions):
chrome.runtime.sendMessage('extensionID', {action: 'test'}, function(response) { console.log(response); });

And finally background.js:
chrome.runtime.onMessageExternal.addListener(function(request, sender, sendResponse) {
    console.log('background.js (onMessageExternal)...');
}

wOxxOm

unread,
Jan 13, 2022, 9:39:25 AM1/13/22
to Chromium Extensions, peter....@gmail.com
Synchronous injection in the main world is indeed broken in MV3 until https://crbug.com/1207006 is implemented or chrome.scripting.registerContentScripts allows specifying `world: 'main'` in https://crbug.com/1054624.

> this new .js file has no access to the chrome api

In MV2 the code injected in the script element also couldn't access the entire `chrome` API due to JS isolation, only the content script code can do it. Note that you always had two scripts: the content script in the isolated world and the DOM script in the main world. You can use DOM messaging to pass the data between the worlds as shown in the well-known StackOverlow topic. Only some types of data are supported per the structured clone algorithm and a runtime `port` is not one of these. In the future there will be a way for secure communication when it's implemented in https://crbug.com/1054624. Of course you can also use external messaging like you did.

> an "undefined" for the sendMessage response

Your onMessageExternal listener should call sendResponse, see this answer where I've summarized these common mistakes.

P.S. An alternative for web_accessible_resources method is chrome.scripting.executeScript with `world: 'main'` in your background script. If the decision to run the code is made in the content script then send a message to the background script so it'll do executeScript using `sender` parameter of onMessage listener, specifically sender.tab.id and sender.frameId. Note that executeScript runs the code only after DOMContentLoaded due to a design oversight.

wOxxOm

unread,
Jan 13, 2022, 9:43:09 AM1/13/22
to Chromium Extensions, wOxxOm, peter....@gmail.com
...just in case, to see console.log of the background script open its own devtools and do it before "(Inactive)" is shown!

H2O2F.png

peter....@gmail.com

unread,
Jan 13, 2022, 10:11:32 AM1/13/22
to Chromium Extensions, wOxxOm, peter....@gmail.com
Wow thank you so much! I can't believe it was that simple! All thanks to the StackOverflow link. I must be so tunnel visioned with extension code/api's...

I added this to my content_load.js file (which is a content_script):
document.addEventListener('uniqueEventDescription', function(e) {
        console.log('received new event...');
        console.log(e);
});

And in the injected javascript file, I replaced:
chrome.runtime.sendMessage('extensionID', {action: 'test'}, function(response) { console.log(response); });
with:
document.dispatchEvent(new CustomEvent('uniqueEventDescription', { detail: "test" }));

I now have access to the appropriate chrome api's because I'm in a content script.
Reply all
Reply to author
Forward
0 new messages