How to modify http request headers based on user options in a Chrome extension?

Viewed 1591

I'm working on my first extension for Google Chrome. I want to add a custom header to (all) http(s) requests based on options that the user can select on the options page of my extension.

I managed the following successfully:

  1. Manually add custom headers to all http requests
  2. Create an options page
  3. Save and restore options in chrome.storage.sync

Now I'm struggling to combine the above. Here's what I tried:

chrome.webRequest.onBeforeSendHeaders.addListener(function(details){
    var demoOptions = new Array();
    var demoHeader = '';
    var bkg = chrome.extension.getBackgroundPage(); // for debugging
    chrome.storage.sync.get({
    demoOption: true,
    }, function(items) {
        if(items.demoOption){
            demoOptions.push('demo-header-1');
        }
        demoHeader = demoOptions.join();
        details.requestHeaders.push({
            name: 'X-Demo-Header',
            value: demoHeader //this is not showing up in the http headers
        });
        bkg.console.log('demoHeader:' + demoHeader); //this is putting the correct value to the debugging console
        bkg.console.log(details.requestHeaders); //this is putting the headers to the debugging console, incl. x-demo-header1 and x-demo-header2 (see below)
    });

    details.requestHeaders.push({
        name: 'X-Demo-Header2',
        value: 'Demo-Header-2' //this is correctly added to the headers and included in the request
    });
    
    return { requestHeaders: details.requestHeaders };
},
{urls: [ "*://*/*" ]},['requestHeaders','blocking']);

Observations:

  1. X-Demo-Header2 is correctly inserted (visible in Chrome header inspection and n the debigging console)
  2. x-demo-header1 is not part of the header sent to the site, but it is visible in the debugging output
  3. x-demo-header2 is in the header before X-Demo-Header1 in the header array in the console (see screenshot)

Assumption: function(items) to fetch my options is executed asynchronyously and requestHeaders are returned before the headers from my options are inserted.

Can I avoid this? Is there a better option to add headers based on options?

Screenshot:

Debugging output of the header array

1 Answers

TL;DR

It currently won't work in Chrome (Feb. 15th 2021)

Long answer

JavaScript heavily relies on the idea of asynchronous code execution. In your code you return { requestHeaders: details.requestHeaders } before the event listener registered in chrome.storage.sync.get(key, listener) has finished. Therefore, demo-header-1 is added too late.

That's why you need to use a Promise. There is an example in the docs how to do that. On that page scroll down to where it says: "This code is exactly like the previous example, except that the listener is asynchronous, returning a Promise which is resolved with the new headers".

In your case the modified version of your code would look this like this:

chrome.webRequest.onBeforeSendHeaders.addListener(
  function (details) {
    return new Promise((resolve, reject) => {
      chrome.storage.sync.get({ demoOption: true }, function (items) {
        if (items.demoOption) {
          details.requestHeaders.push({
            name: 'X-Demo-Header',
            value: 'demo-header-1',
          });
        }
        resolve({ requestHeaders: details.requestHeaders });
      });
    });
  },
  { urls: ['*://*/*'] },
  ['blocking', 'requestHeaders']
);

As you can see we now return a Promise instead of an object literal. In this Promise we wait for the read operation of chrome.storage.sync.get to finish. Once it's done we resolve the Promise.

This should work in browsers which support this.

In Chrome this doesn't work because:

There is no support in Chrome for returning a Promise from webRequest.onBeforeSendHeaders since 2018

Unless you can find a way to load the data from chrome.storage.sync.get in a synchronous way you are out of luck I'm afraid.

As a last resort you can try to post a complaint in that Chromium issue.

Related