vkuzel.com

Adding custom HTTP headers to HTML links

2021-04-05

Goal of this task is to intercept a request created by a simple <a href="..."> element, and to add a custom HTTP header into the request. This should be done transparently, without XMLHttpRequest or similar method which can be problematic if new page is opened or a big file should be downloaded.

To accomplish this, we will:

  1. Register a Service Worker intercepting requests.
  2. Augment intercepted requests with a new header, and forward those requests to a server.

To register a Service Worker we have to call the ServiceWorkerContainer.register() method.

Note that a Service Worker cannot be registered in a HTML page opened with the file:// protocol. It has to be served by a web server.

// index.html
window.addEventListener('load', function () {
    navigator
        .serviceWorker
        .register('/request-interceptor.js')
        .then(function (registration) {
            console.log('Service worker registered with scope: ', registration.scope);
        }, function (err) {
            console.log('ServiceWorker registration failed: ', err);
        });
});

In the Service Worker the FetchEvent is captured, a request is extracted and augmented with the new X-Custom-Header header, and forwarded by calling the WindowOrWorkerGlobalScope.fetch() method.

// request-interceptor.js
self.addEventListener('fetch', function (event) {
    event.respondWith(async function () {
        let headers = new Headers()
        headers.append("X-Custom-Header", "Random value")
        return fetch(event.request, {headers: headers})
    }());
});

In the Chrome browser, modified requests can be observed in the Developer Tools console on the Network tab.

Note: The origin (referer) of the request is no longer page itself, but the Service Worker itself.

Chrome: augmented request

Unfortunately Firefox and Safari does not show augmented requests at this moment. To verify requests are sent correctly we can use tcpdump or we can log requests on a server.

In this example the server is running on the localhost port 8080, so the tcpdump is listening on the loopback lo0 interface.

$ tcpdump -A -i lo0 port 8080
...
9...9...GET /index.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:87.0) Gecko/20100101 Firefox/87.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/request-interceptor.js
x-custom-header: Random value
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
...