Injecting javascript variable before content script

This is going to be very tricky.

Let’s look at your requirements.

Inject.js will need to have access to this variable and run it’s code BEFORE any scripts on the page run.

That’s not how your code currently works. Your inject.js is executed at document_end – which happens after the whole DOM tree is parsed, which means after all page scripts have run (barring asynchronous parts and async script loading).

Chrome has a solution to that – you can set your execution to document_start. Then your code will truly run before everything else, while DOM is still not parsed (so document is essentially empty). With what your code does, it should not create problems (it only relies on document.documentElement, which will exist).

Problem is, all your code has to be synchronous to still enjoy “runs before everything else” property. Chrome will pause DOM parsing as long as the synchronous part of your code runs, but then all bets are off as it merrily continues to parse (and run code from) the document.

This, for example, disqualifies chrome.storage and Messaging as access to that is necessarily asynchronous.

I need to inject a dynamic variable [on a page load]

Meaning that you cannot store this in advance in some synchronously-available storage (e.g. in localStorage or cookies of the website), which would be problematic anyway considering you don’t know domains in advance.

Note, for your code in particular, this may not be that much of a factor; your “dynamic” value is in fact fixed per domain. You still don’t know in advance which domain will be visited, but you can at least guarantee that on a second load it will be there.

Using my background script background.js, I need to inject a dynamic variable as a content script before injecting another file [that still needs to run before everything else on the page]

That’s the tricky part. In fact, as stated, it’s simply impossible. You’re trying to catch, from the background, the exact moment between the navigation being committed, so that Chrome switched the page to the new domain, and the execution of your document_start script.

There is no detectable gap there, and no way to tell Chrome to wait. It’s a race condition you have no hopes to resolve.

You’re trying to use webNavigation.onBeforeNavigate – before even the navigation is committed. So your injectScript probably goes to the previous page even, rendering it useless. If you try some other event, e.g . onCommitted, there’s still no telling as to when exactly injectScript will be treated. Likely after your script.

So, how to work around all this?

Fortunately, there is some synchronous storage that’s available to the content script that you can push some information to right before the earliest of scripts executes.

Cookies.

However, using the chrome.cookies API won’t help. You need to actively inject the cookie value into the request on webRequest.onHeadersReceived.

You have to have the value ready synchronously to process it with a blocking handler to onHeadersReceived, but then you can simply add one Set-Cookie header and have it immediately available in document.cookies in your inject.js.

  • background.js

    function addSeedCookie(details) {
      seed = SomethingSynchronous();
      details.responseHeaders.push({
        name: "Set-Cookie",
        value: `seed_goes_here=${seed};`
      });
      return {
        responseHeaders: details.responseHeaders
      };
    }
    
    chrome.webRequest.onHeadersReceived.addListener(
      addSeedCookie, {urls: ["<all_urls>"]}, [
        "blocking",
        "responseHeaders",
        // Chrome 72+ requires 'extraHeaders' to handle Set-Cookie header
        chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
      ].filter(Boolean)
    );
    
  • inject.js

    function getCookie(cookie) { // https://stackoverflow.com/a/19971550/934239
      return document.cookie.split(';').reduce(function(prev, c) {
        var arr = c.split('=');
        return (arr[0].trim() === cookie) ? arr[1] : prev;
      }, undefined);
    }
    
    var seed = getCookie("seed_goes_here");
    

If an asynchronous function is needed to produce the data, prepare the data before the request is sent in onBeforeRequest event, then use it in onHeadersReceived listener.

const preparedSeed = {};
chrome.webRequest.onBeforeRequest.addListener(
  details => {
    chrome.storage.local.get('seed', data => {
      preparedSeed[details.requestId] = data.seed;
    });
  }, {
    urls: ['<all_urls>'],
    types: ['main_frame', 'sub_frame'],
  });

Note: the above code is untested and only serves to illustrate the idea.

Leave a Comment