avoiding duplicate content script injections

1,570 views
Skip to first unread message

Nathan Peterson

unread,
Dec 14, 2022, 9:30:58 PM12/14/22
to Chromium Extensions
I'm working on solving a tricky problem that results in duplicate content script injections and curious if anyone in this group has insight or suggestions.

We inject our content script programmatically using `exectureScript` from the background page in response to navigation events. The intended flow goes something like this:
1. user navigates to supported url
2. webNavigation event fires in background script
3. background script injects content script

The problem arises when users are quickly redirected through multiple supported urls (e.g. saml flow). We have code in place to try and prevent duplicate injections but in the course of rapid redirects, the navigation listener fires twice. Here's the central problem – the content script is sizable and it takes a long time for the browser to parse all the JS. So the content script that was meant to be injected into "foobar/login?redirect" actually gets injected into "foobar/home." Then, the second webNavigation event also triggers in "foobar/home" – now that the navigation sequence is over we end up with duplicate scripts.

Have others run into this issue? Do you have any strategies to recommend?

Another approach that I've considered is sending a message from background to content after the navigation event to check whether there is already a content script there. The problem with this solution is that when there is NOT a content script in the host page, the callback for `sendMessage` never fires because there is no response. So we need wait an arbitrary length of time to see if the content script will respond before injecting. By the time the arbitrary time has elapsed, the host page may have navigated again or the service worker may fall asleep...

Anyway, curious to hear if others have encountered similar issues.

Simeon Vincent

unread,
Dec 14, 2022, 10:59:51 PM12/14/22
to Nathan Peterson, Chromium Extensions
This problem can be rather neatly addressed by tweaking your webNavigation listener and executeScript calls to use documentId instead of frameId.

From the webNavigation API docs:

Another concept that is problematic with extensions is the lifecycle of the frame. A frame hosts a document (which is associated with a committed URL). The document can change (say by navigating) but the frameId won’t, and so it is difficult to associate that something happened in a specific document with just frameIds. We are introducing a concept of a documentId which is a unique identifier per document. If a frame is navigated and opens a new document the identifier will change. 

This concept is relatively new. If you have to support older versions of Chrome (before ~106 I think?) you'll need to use another strategy to avoid duplicate execution. The most direct solution that leaps to mind is to wrap the injected script in a conditional so that the body only runs if it hasn't yet. For example:

//// content-script.js
if (typeof contentScriptHasRun === 'undefined') {
// Script body

var contentScriptHasRun = true;
}

Simeon - @dotproto
Chrome Extensions DevRel


--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/chromium-extensions/58f8c948-7bb7-4f88-8855-fa5740828666n%40chromium.org.

wOxxOm

unread,
Dec 15, 2022, 9:38:04 AM12/15/22
to Chromium Extensions, Simeon Vincent, Chromium Extensions, natp...@gmail.com
+1 for documentId

Also when using the typeof check in old browsers:
1. beware of implicit auto-created globals for elements with id: this check will fail if the site has an element like <html id="contentScriptHasRun">
2. split the content script so the main script will be just a loader

// using `1` to guard against implicit auto-created globals for elements with id
if (window.INJECTED !== 1) {
   window.INJECTED = 1;
  // content2.js must be exposed via web_accessible_resources
  import(chrome.runtime.getURL('content2.js'));
}

Another solution - if the URL condition can be expressed via match patterns - is to use chrome.scripting.registerContentScripts just once e.g. in chrome.runtime.onInstalled.

Nathan Peterson

unread,
Dec 15, 2022, 10:43:10 PM12/15/22
to Chromium Extensions, wOxxOm, Simeon Vincent, Chromium Extensions, Nathan Peterson
Thanks both. `documentId` sounds like it would be a perfect solution but we do support a long tail of older chrome versions! 

Both approaches with wrapping the content script are variations of what we are doing now but there are some helpful ideas in your answers. Really appreciate the responses!

wOxxOm

unread,
Dec 16, 2022, 4:25:37 AM12/16/22
to Chromium Extensions, natp...@gmail.com, wOxxOm, Simeon Vincent, Chromium Extensions
If you intentionally support old browsers then another solution is chrome.declarativeContent API with RequestContentScript action, which despite its experimental status in the documentation is working properly in the stable Chrome for JS (there are bugs with CSS injection). It should be registered in chrome.runtime.onInstalled.
Reply all
Reply to author
Forward
0 new messages