Intercept XHR and change request headers and url before send in JavaScript

Viewed 2042

I want to intercept all XHR requests being sent, and change their URL and headers before the request gets sent.
Found this similar question but there are no answers there.

I tried hooking XMLHttpRequest.prototype.open, But it only gives me access to the response:

(function () {
    var origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function () {
        console.log(arguments); // prints method ("GET"), URL
        console.log(this); // prints response, responseText, responseURL, status, statusText, and onXXX handlers
        origOpen.apply(this, arguments);
    };
})();

Also tried hooking XMLHttpRequest.prototype.setRequestHeader, but it only gives me access to each header value being set, one by one, and I can't associate it to the URL of the request:

(function () {
    var origSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
    XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
        console.log("header", header);
        console.log("value", value);
        origSetRequestHeader.apply(this, arguments);
    };
})();

I managed to hook XMLHttpRequest.prototype.send to set a custom header, but since I want to change an existing header key, it appends my new value instead of replacing the existing one. Other people encountered the same problem: 1, 2:

(function () {
    var origSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function () {
        arguments[1] = myNewUrl; // arguments[1] holds the URL
        this.setRequestHeader('existingHeaderKey', newHeaderValue)
        origSend.apply(this, arguments);
    };
})();

How can I accomplish this?

1 Answers

The XMLHttpRequest(xhr) interface exposes a very few things. So there is limitation on what you can intercept.

However, we can wrap the xhr objects in Proxy and collect data until send is called. And before sending the request we modify the data at one spot.

const OriginalXHR = XMLHttpRequest;

// wrap the XMLHttpRequest
XMLHttpRequest = function() {
  return new Proxy(new OriginalXHR(), {

    open(method, url, async, username = null, password = null) {
      lg('open');
      // collect URL and HTTP method
      this.modMethod = method;
      this.modUrl = url;

      this.open(...arguments);
    },

    setRequestHeader(name, value) {
      lg('set header');
      if (!this.modReqHeaders) {
        this.modReqHeaders = {};
      }
      // collect headers
      this.modReqHeaders[name] = value;

      // do NOT set headers here. Hold back!
      // this.setRequestHeader(name, value);
    },

    send(body = null) {
      lg('processing request...');
      // do the final processing
      // ...
      // don't forget to set headers
      for (const [name, value] of Object.entries(this.modReqHeaders)) {
        this.setRequestHeader(name, value);
      }

      lg('sending request =>' +
        '\n\t\tmethod: \t' + this.modMethod +
        '\n\t\turl:\t\t' + this.modUrl +
        '\n\t\theaders:\t' + JSON.stringify(this.modReqHeaders));
      this.send(body);
    },

    get(xhr, key) {
      if (!key in xhr) return undefined;

      let value = xhr[key];
      if (typeof value === "function") {
        // if wrapped, use the function in proxy
        value = this[key] || value;
        return (...args) => value.apply(xhr, args);
      } else {
        //return properties
        return value;
      }
    },

    set(xhr, key, value) {
      if (key in xhr) {
        xhr[key] = value;
      }
      return value;
    }
  });
}
console.warn('XMLHttpRequest has been patched!\n XMLHttpRequest: ', XMLHttpRequest);

let url = 'https://baconipsum.com/api/?type=all-meat&sentences=1&start-with-lorem=1';

function getData() {
  console.log('fetching lorem ipsum');
  let xhr = new XMLHttpRequest();
  xhr.responseType = 'json';

  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById("demo").innerText = this.response[0];
    }
  };
  xhr.open("GET", url, true);
  xhr.setRequestHeader('Referer', 'www.google.com');
  xhr.setRequestHeader('Accept-Encoding', 'x-compress; x-zip')
  xhr.setRequestHeader('Accept-Language', 'de-US,en;q=0.5');
  xhr.send();
}

//fancy logging, looks good in dark mode
function lg(msg) {
  console.log('%c\t Proxy: ' + msg, 'background: #222; color: #bada55');
}
#demo {
  min-height: 100px;
  background-color: wheat;
}
<button onclick="getData()">Get data</button>
<div id="demo"></div>
<p>Note: look in the Developer Console for debug logs</p>

You can wrap remaining xhr methods or attributes in the proxy handler as per your requirement.
This may not be as good as service workers. But service workers have following drawbacks:

A service worker is run in a worker context: it therefore has no DOM access, and runs on a different thread to the main JavaScript that powers your app, so it is non-blocking. It is designed to be fully async; as a consequence, APIs such as synchronous XHR and Web Storage can't be used inside a service worker.

Service workers only run over HTTPS, for security reasons. Having modified network requests, wide open to man in the middle attacks would be really bad. In Firefox, Service Worker APIs are also hidden and cannot be used when the user is in private browsing mode.ref

Related