umpljazz
umpljazz

Reputation: 1222

Intercept XMLHttpRequest and modify responseText

I'm trying to build a script that will act as a proxy/wrapper for the native XMLHttpRequest object enabling me to intercept it, modify the responseText and return back to the original onreadystatechange event.

The context being, if the data the app is trying to receive is already available in local storage, to abort the XMLHttpRequest and pass the locally stored data back into the apps success/failure callback methods. Assume I have no control over the apps existing AJAX callback methods.

I had originally tried the following idea..

var send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(data){
   //Do some stuff in here to modify the responseText
   send.call(this, data);
};

But as I have now established, the responseText is read only.

I then tried taking a step back, writing my own full native proxy to XMLHttpRequest, ultimately ending up writing my own version of the native methods. Similar to what is discussed here...

http://www.ilinsky.com/articles/XMLHttpRequest/#implementation-wrapping

But it rapidly got confusing, and still have the difficulty of returning the modified data back into the original onReadyStateChange method.

Any suggestions? Is this even possible?

Upvotes: 18

Views: 29534

Answers (5)

Zezombye
Zezombye

Reputation: 333

The accepted answer used to work for me on Twitter up until recently for some reason. So here's another one that works:

        var open_prototype = XMLHttpRequest.prototype.open
        unsafeWindow.XMLHttpRequest.prototype.open = function() {
            this.addEventListener('readystatechange', function(event) {
                if ( this.readyState === 4 ) {
                    var response = event.target.responseText.replaceAll("a", "b");
                    Object.defineProperty(this, 'response', {writable: true});
                    Object.defineProperty(this, 'responseText', {writable: true});
                    this.response = this.responseText = response;
                }
            });
            return open_prototype.apply(this, arguments);
        };

You can filter on URL with event.target.responseURL

Upvotes: 1

ipid
ipid

Reputation: 652

In my opinion, to intercept the response of XMLHttpRequest, a more modern solution will be extending the original XMLHttpRequest and overwrite it in the window object:

const { interceptXhrResponse } = (function () {
  let interceptionRules = [];

  /**
   * Function to intercept responses for given URL patterns
   * @param {RegExp} urlPattern - Regular expression to match the (canonicalized) URL
   * @param {Function} responseHandler - Function to handle the intercepted response
   */
  function interceptXhrResponse(urlPattern, responseHandler) {
    interceptionRules.push({ urlPattern, responseHandler });
  }

  // Function to find specific handler for the URL and return modified response
  function handleInterceptedResponse(response, url) {
    const interceptionRule = interceptionRules.find(({ urlPattern }) =>
      urlPattern.test(url)
    );

    if (interceptionRule) {
      const { responseHandler } = interceptionRule;
      return responseHandler(response, url);
    }

    return response;
  }

  const OriginalXMLHttpRequest = window.XMLHttpRequest;

  class XMLHttpRequest extends OriginalXMLHttpRequest {
    get responseText() {
      // If the request is not done, return the original responseText
      if (this.readyState !== 4) {
        return super.responseText;
      }

      return handleInterceptedResponse(super.responseText, this.responseURL);
    }

    get response() {
      // If the request is not done, return the original response
      if (this.readyState !== 4) {
        return super.response;
      }

      return handleInterceptedResponse(super.response, this.responseURL);
    }
  }

  window.XMLHttpRequest = XMLHttpRequest;

  return { interceptXhrResponse };
})();

The upper code exposes the interceptXhrResponse function, allowing you to specify a URL pattern with a regular expression and a corresponding response handler. You can return everything you want in the handler to change the response.

For example:

interceptXhrResponse(/.+/, (response, url) => {
  return `Response of ${url}: Intercepted. Original response length: ${String(response).length}`
})

Then we can try to initiate an XMLHttpRequest:

const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://stackoverflow.com/404')
xhr.send()
xhr.onloadend = () => {
  console.log(xhr.responseText)
}

Output:

Response of https://stackoverflow.com/404: Intercepted. Original response length: 63486

Upvotes: 2

EtherDream
EtherDream

Reputation: 174

//
// firefox, ie8+ 
//
var accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');

Object.defineProperty(XMLHttpRequest.prototype, 'responseText', {
	get: function() {
		console.log('get responseText');
		return accessor.get.call(this);
	},
	set: function(str) {
		console.log('set responseText: %s', str);
		//return accessor.set.call(this, str);
	},
	configurable: true
});


//
// chrome, safari (accessor == null)
//
var rawOpen = XMLHttpRequest.prototype.open;

XMLHttpRequest.prototype.open = function() {
	if (!this._hooked) {
		this._hooked = true;
		setupHook(this);
	}
	rawOpen.apply(this, arguments);
}

function setupHook(xhr) {
	function getter() {
		console.log('get responseText');

		delete xhr.responseText;
		var ret = xhr.responseText;
		setup();
		return ret;
	}

	function setter(str) {
		console.log('set responseText: %s', str);
	}

	function setup() {
		Object.defineProperty(xhr, 'responseText', {
			get: getter,
			set: setter,
			configurable: true
		});
	}
	setup();
}

Upvotes: 15

Abhishek Kumar
Abhishek Kumar

Reputation: 21

The following script perfectly intercept the data before sending via XMLHttpRequest.prototype.send

<script>
(function(send) { 

        XMLHttpRequest.prototype.send = function(data) { 

            this.addEventListener('readystatechange', function() { 

            }, false); 

            console.log(data); 
            alert(data); 

        }; 

})(XMLHttpRequest.prototype.send);
</script>

Upvotes: 2

Jan Turoň
Jan Turoň

Reputation: 32912

Your step-back is an overkill: you may add your own getter on XMLHttpRequest: (more about properties)

Object.defineProperty(XMLHttpRequest.prototype,"myResponse",{
  get: function() {
    return this.responseText+"my update"; // anything you want
  }
});

the usage:

var xhr = new XMLHttpRequest();
...
console.log(xhr.myResponse); // xhr.responseText+"my update"

Note on modern browsers you may run xhr.onload (see XMLHttpRequest2 tips)

Upvotes: 0

Related Questions