How can I modify the XMLHttpRequest responsetext received by another function?
Asked Answered
L

9

32

I am trying to modify the responseText received by a function that I cannot modify. This function creates a XMLHttpRequest that I can attach to, but I have been unable to "wrap" the responseText in a way that allows me to modify the content before the original function receives it.

Here's the full original function:

function Mj(a, b, c, d, e) {
    function k() {
        4 == (m && 'readyState' in m ? m.readyState : 0) && b && ff(b) (m)
    }
    var m = new XMLHttpRequest;
    'onloadend' in m ? m.addEventListener('loadend', k, !1)  : m.onreadystatechange = k;
    c = ('GET').toUpperCase();
    d = d || '';
    m.open(c, a, !0);
    m.send(d);
    return m
}
function ff(a) {
    return a && window ? function () {
        try {
            return a.apply(this, arguments)
        } catch(b) {
            throw jf(b),
                b;
        }
    } : a
}

I have also tried to manipulate the reiceiving function k(); in an attempt to reach my goal, but since it doesn't depend on any data passing to the function (for example k(a.responseText);) I had no success.

Is there any way that I can achieve this? I do not wish to use js libraries (such as jQuery);


EDIT: I understand that I cannot change .responseText directly since it is read-only, but I am trying to find a way to change the content between the response and receiving function.


EDIT2: Added below one of the methods I have tried to intercept and change .responseText which has been addapted from here: Monkey patch XMLHTTPRequest.onreadystatechange

(function (open) {
XMLHttpRequest.prototype.open = function (method, url, async, user, pass) {
    if(/results/.test(url)) {
      console.log(this.onreadystatechange);
        this.addEventListener("readystatechange", function () {
            console.log('readystate: ' + this.readyState);
            if(this.responseText !== '') {
                this.responseText = this.responseText.split('&')[0];
            }
        }, false);
    }
    open.call(this, method, url, async, user, pass);
};
})(XMLHttpRequest.prototype.open);

EDIT3: I forgot to include that the functions Mj and ff are not globally available, they are both contained inside an anonymous function (function(){functions are here})();


EDIT4: I have changed the accepted answer because AmmarCSE's does not have any of the problems and complexity linked to jfriend00's answer.

The best answer explained in short is as follows:

Listen to whichever request you want to modify (make sure your listener will intercept it before the original function destination does, otherwise there is no point in modifying it after the response has already been used).

Save the original response (if you want to modify it) in a temporary variable

Change the property you want to modify to "writable: true", it will erase whichever value it had. In my case I use

Object.defineProperty(event, 'responseText', {
    writable: true
});

Where event is the object returned by listening to the load or readystatechange event of the xhr request

Now you can set anything you want for your response, if all you wanted was to modify the original response then you can use that data from your temporary variable and then save the modifications in the response.

Legitimate answered 19/10, 2014 at 4:34 Comment(2)
As this meta thread notes, answers should not be part of the question but should be in their own answer. I have moved your answer to a community wiki answer (so I don't get rep from it) and edited it out of the question. You can edit the community wiki answer if you want to rephrase the post.Autonomy
@Autonomy Appreciate the help and information, the community wiki answer sounds good.Legitimate
S
15

One very simple workaround is to change the property descriptor for responseText itself

Object.defineProperty(wrapped, 'responseText', {
     writable: true
});

So, you can extend XMLHttpRequest like

(function(proxied) {
    XMLHttpRequest = function() {
        //cannot use apply directly since we want a 'new' version
        var wrapped = new(Function.prototype.bind.apply(proxied, arguments));

        Object.defineProperty(wrapped, 'responseText', {
            writable: true
        });

        return wrapped;
    };
})(XMLHttpRequest);

Demo

Stoke answered 30/5, 2016 at 19:31 Comment(7)
I have always wondered if such method wouldn't be possible and perhaps easier, but whenever I tried to change it through defineProperty I would end up with endless recursive loops (responseText changes responseText which changes responseText, etc.). I will definitely give this one a try, its simplicity is perfect for the intended use.Legitimate
This option does not appear to be appropriate since it completely empties whatever responseText is returning, the result is responseText always returning "undefined" despite being able to add values to it. The intention is to modify responseText, not erase it and replace completely with new values.Legitimate
I finally figured out how to make this work. During the interception is when that specific XMLHttpRequest must have its property overridden, right before the property needs to be modified. If it is done before then the responseText value, for example, is lost.Legitimate
@Legitimate Do you have a snippet to show how your final solution was done?Youngs
@Youngs I have included an example snippet at the end of my question, in EDIT5Legitimate
Using your code, the original responseText is always undefined. So if the real responseText is important, you should be aware of that. I've commented out your response change in your demo code, and "undefined" is returned - jsfiddle.net/320g5m08/4Leralerch
Try stackoverflow.com/a/41609223. It works for me at 2023 with Chrome.Uptodate
S
24

Edit: See the second code option below (it is tested and works). The first one has some limitations.


Since you can't modify any of those functions, it appears you have to go after the XMLHttpRequest prototype. Here's one idea (untested, but you can see the direction):

(function() {
    var open = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
        var oldReady;
        if (async) {   
            oldReady = this.onreadystatechange;
            // override onReadyStateChange
            this.onreadystatechange = function() {
                if (this.readyState == 4) {
                    // this.responseText is the ajax result
                    // create a dummay ajax object so we can modify responseText
                    var self = this;
                    var dummy = {};
                    ["statusText", "status", "readyState", "responseType"].forEach(function(item) {
                        dummy[item] = self[item];
                    });
                    dummy.responseText = '{"msg": "Hello"}';
                    return oldReady.call(dummy);
                } else {
                    // call original onreadystatechange handler
                    return oldReady.apply(this, arguments);
                }
            }
        } 
        // call original open method
        return open.apply(this, arguments);
    }

})();

This does a monkey patch for the XMLHttpRequest open() method and then when that is called for an async request, it does a monkey patch for the onReadyStateChange handler since that should already be set. That patched function then gets to see the responseText before the original onReadyStateChange handler is called so it can assign a different value to it.

And, finally because .responseText is ready-only, this substitutes a dummy XMLHttpResponse object before calling the onreadystatechange handler. This would not work in all cases, but will work if the onreadystatechange handler uses this.responseText to get the response.


And, here's an attempt that redefines the XMLHttpRequest object to be our own proxy object. Because it's our own proxy object, we can set the responseText property to whatever we want. For all other properties except onreadystatechange, this object just forwards the get, set or function call to the real XMLHttpRequest object.

(function() {
    // create XMLHttpRequest proxy object
    var oldXMLHttpRequest = XMLHttpRequest;

    // define constructor for my proxy object
    XMLHttpRequest = function() {
        var actual = new oldXMLHttpRequest();
        var self = this;

        this.onreadystatechange = null;

        // this is the actual handler on the real XMLHttpRequest object
        actual.onreadystatechange = function() {
            if (this.readyState == 4) {
                // actual.responseText is the ajax result

                // add your own code here to read the real ajax result
                // from actual.responseText and then put whatever result you want
                // the caller to see in self.responseText
                // this next line of code is a dummy line to be replaced
                self.responseText = '{"msg": "Hello"}';
            }
            if (self.onreadystatechange) {
                return self.onreadystatechange();
            }
        };

        // add all proxy getters
        ["status", "statusText", "responseType", "response",
         "readyState", "responseXML", "upload"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];}
            });
        });

        // add all proxy getters/setters
        ["ontimeout, timeout", "withCredentials", "onload", "onerror", "onprogress"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];},
                set: function(val) {actual[item] = val;}
            });
        });

        // add all pure proxy pass-through methods
        ["addEventListener", "send", "open", "abort", "getAllResponseHeaders",
         "getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
            Object.defineProperty(self, item, {
                value: function() {return actual[item].apply(actual, arguments);}
            });
        });
    }
})();

Working demo: http://jsfiddle.net/jfriend00/jws6g691/

I tried it in the latest versions of IE, Firefox and Chrome and it worked with a simple ajax request.

Note: I have not looked into all the advanced ways that Ajax (like binary data, uploads, etc...) can be used to see that this proxy is thorough enough to make all those work (I would guess it might not be yet without some further work, but it is working for basic requests so it looks like the concept is capable).


Other attempts that failed:

  1. Tried to derive from the XMLHttpRequest object and then replace the constructor with my own, but that didn't work because the real XMLHttpRequest function won't let you call it as a function to initialize my derived object.

  2. Tried just overriding the onreadystatechange handler and changing .responseText, but that field is read-only so you can't change it.

  3. Tried creating a dummy object that is sent as the this object when calling onreadystatechange, but a lot of code doesn't reference this, but rather has the actual object saved in a local variable in a closure - thus defeating the dummy object.

Splutter answered 19/10, 2014 at 5:54 Comment(14)
But I cannot assign a new value to responseText because it is read only, otherwise my previous methods -which are very similar to the one you shared- would have worked. I did try your code nonetheless and the responseText value remains unchanged as expected.Legitimate
@Legitimate - I'm working on it. Almost there. I'll post a comment to you when I get it.Splutter
I greatly appreciate it. I am here thinking if it's possible to emulate the XMLHttprequest functions in order to create a "man-in-the-middle" which would stop the original calls, make the xmlhttpresponses himself and then return the modified data to the function. Basically an improved clone of xmlhhtprequest, like an emulator.Legitimate
@Legitimate - Look at my second code block in my answer. I created a proxy object, replacing the XMLHttpRequest constructor with my own proxy object constructor which then allows me to modify .responseText.Splutter
That is incredible, works perfectly. I also had to add an extra line if(this.readyState == 3){self.responseText = this.responseText} Because it was breaking some of my own requests. I have selected yours as the correct answer and will study more the technique that you have used. I am really greatful for the help that you provided to me.Legitimate
@Legitimate - it may be that you should just assign the responseText anytime that handler is called.Splutter
So the best precaution should be instead if(this.readyState != 4){self.responseText = this.responseText} to account for any other possible functions that try to get the response before readyState == 4?Legitimate
@Legitimate - you can see how I modified it here: jsfiddle.net/jfriend00/jws6g691Splutter
@Legitimate - here's a bit safer version (particularly for future use) that proxies all properties on the XMLHttpRequest object (by iterating over them) rather than listing only a specific list of properties: jsfiddle.net/jfriend00/banxvu2cSplutter
Wow, that is incredible. Your help and the added notes have been amazing and I am starting to learn a great deal more about Object.defineProperty thanks to you.Legitimate
@Legitimate - you'll notice I added support for .onload and .addEventListener("load", ...) which are two other ways to see when the xhr request is done. Your code wasn't using those, but I figured I'd make it more complete.Splutter
I did notice and I am really impressed by how I am able to intervene in any of those steps at my will, for example: check if the request is being made towards a certain URL and modify or not the responseText accordingly if I want to change the responses coming from that url. Again, thank you very much for this outstanding help.Legitimate
The first code snippet doesn't seem to work in Chrome 48. oldReady is simply null.Wershba
@Wershba the first works the same in Chrome 48 as Chrome 28 - you've hit one of the (unspecified) limitations the first method has - i.e. the order in which the calling code does things is extremely important - .open must be called after .onreadystatechange =Graduated
E
19

I needed to intercept and modify a request response so I came up with a little bit of code. I also found that some websites like to use response as well as the responseText which is why my code modifies both.

The Code

var open_prototype = XMLHttpRequest.prototype.open,
intercept_response = function(urlpattern, callback) {
   XMLHttpRequest.prototype.open = function() {
      arguments['1'].match(urlpattern) && this.addEventListener('readystatechange', function(event) {
         if ( this.readyState === 4 ) {
            var response = callback(event.target.responseText);
            Object.defineProperty(this, 'response',     {writable: true});
            Object.defineProperty(this, 'responseText', {writable: true});
            this.response = this.responseText = response;
         }
      });
      return open_prototype.apply(this, arguments);
   };
};

the first param of the intercept_response function is a regular expression to match the request url and the second param is the function to be used on the response to modify it.

Example Of Usage

intercept_response(/fruit\.json/i, function(response) {
   var new_response = response.replace('banana', 'apple');
   return new_response;
});
Echeverria answered 31/3, 2017 at 15:33 Comment(0)
S
15

One very simple workaround is to change the property descriptor for responseText itself

Object.defineProperty(wrapped, 'responseText', {
     writable: true
});

So, you can extend XMLHttpRequest like

(function(proxied) {
    XMLHttpRequest = function() {
        //cannot use apply directly since we want a 'new' version
        var wrapped = new(Function.prototype.bind.apply(proxied, arguments));

        Object.defineProperty(wrapped, 'responseText', {
            writable: true
        });

        return wrapped;
    };
})(XMLHttpRequest);

Demo

Stoke answered 30/5, 2016 at 19:31 Comment(7)
I have always wondered if such method wouldn't be possible and perhaps easier, but whenever I tried to change it through defineProperty I would end up with endless recursive loops (responseText changes responseText which changes responseText, etc.). I will definitely give this one a try, its simplicity is perfect for the intended use.Legitimate
This option does not appear to be appropriate since it completely empties whatever responseText is returning, the result is responseText always returning "undefined" despite being able to add values to it. The intention is to modify responseText, not erase it and replace completely with new values.Legitimate
I finally figured out how to make this work. During the interception is when that specific XMLHttpRequest must have its property overridden, right before the property needs to be modified. If it is done before then the responseText value, for example, is lost.Legitimate
@Legitimate Do you have a snippet to show how your final solution was done?Youngs
@Youngs I have included an example snippet at the end of my question, in EDIT5Legitimate
Using your code, the original responseText is always undefined. So if the real responseText is important, you should be aware of that. I've commented out your response change in your demo code, and "undefined" is returned - jsfiddle.net/320g5m08/4Leralerch
Try stackoverflow.com/a/41609223. It works for me at 2023 with Chrome.Uptodate
A
6

By request I include below an example snippet showing how to modify the response of a XMLHttpRequest before the original function can receive it.

// In this example the sample response should be
// {"data_sample":"data has not been modified"}
// and we will change it into
// {"data_sample":"woops! All data has gone!"}

/*---BEGIN HACK---------------------------------------------------------------*/

// here we will modify the response
function modifyResponse(response) {

    var original_response, modified_response;

    if (this.readyState === 4) {

        // we need to store the original response before any modifications
        // because the next step will erase everything it had
        original_response = response.target.responseText;

        // here we "kill" the response property of this request
        // and we set it to writable
        Object.defineProperty(this, "responseText", {writable: true});

        // now we can make our modifications and save them in our new property
        modified_response = JSON.parse(original_response);
        modified_response.data_sample = "woops! All data has gone!";
        this.responseText = JSON.stringify(modified_response);

    }
}

// here we listen to all requests being opened
function openBypass(original_function) {

    return function(method, url, async) {

        // here we listen to the same request the "original" code made
        // before it can listen to it, this guarantees that
        // any response it receives will pass through our modifier
        // function before reaching the "original" code
        this.addEventListener("readystatechange", modifyResponse);

        // here we return everything original_function might
        // return so nothing breaks
        return original_function.apply(this, arguments);

    };

}

// here we override the default .open method so that
// we can listen and modify the request before the original function get its
XMLHttpRequest.prototype.open = openBypass(XMLHttpRequest.prototype.open);
// to see the original response just remove/comment the line above

/*---END HACK-----------------------------------------------------------------*/

// here we have the "original" code receiving the responses
// that we want to modify
function logResponse(response) {

    if (this.readyState === 4) {

        document.write(response.target.responseText);

    }

}

// here is a common request
var _request = new XMLHttpRequest();
_request.open("GET", "https://gist.githubusercontent.com/anonymous/c655b533b340791c5d49f67c373f53d2/raw/cb6159a19dca9b55a6c97d3a35a32979ee298085/data.json", true);
_request.addEventListener("readystatechange", logResponse);
_request.send();
Autonomy answered 19/10, 2014 at 4:34 Comment(0)
A
6

You can wrap the getter for responseText in the prototype with a new function and make the changes to the output there.

Here is a simple example that appends the html comment <!-- TEST --> to the response text:

(function(http){
  var get = Object.getOwnPropertyDescriptor(
    http.prototype,
    'responseText'
  ).get;

  Object.defineProperty(
    http.prototype,
    "responseText",
    {
      get: function(){ return get.apply( this, arguments ) + "<!-- TEST -->"; }
    }
  );
})(self.XMLHttpRequest);

The above function will change the response text for all requests.

If you want to make the change to just one request then do not use the function above but just define the getter on the individual request instead:

var req = new XMLHttpRequest();
var get = Object.getOwnPropertyDescriptor(
  XMLHttpRequest.prototype,
  'responseText'
).get;
Object.defineProperty(
  req,
  "responseText", {
    get: function() {
      return get.apply(this, arguments) + "<!-- TEST -->";
    }
  }
);
var url = '/';
req.open('GET', url);
req.addEventListener(
  "load",
   function(){
     console.log(req.responseText);
   }
);
req.send();
Autonomy answered 12/1, 2017 at 9:19 Comment(0)
F
3

I ran into the same problem when I was making a Chrome extension to allow cross origin API calls. This worked in Chrome. (Update: It doesn't work in the newest Chrome version).

delete _this.responseText;
_this.responseText = "Anything you want";

The snippet runs inside a monkeypatched XMLHttpRequest.prototype.send who is redirecting the requests to the extensions background script and replace all the properties on response. Like this:

// Delete removes the read only restriction
delete _this.response;
_this.response = event.data.response.xhr.response;
delete _this.responseText;
_this.responseText = event.data.response.xhr.responseText;
delete _this.status;
_this.status = event.data.response.xhr.status;
delete _this.statusText;
_this.statusText = event.data.response.xhr.statusText;
delete _this.readyState;
_this.readyState = event.data.response.xhr.readyState;

That didn't work in Firefox, but I found a solution that worked:

var test = new XMLHttpRequest();
Object.defineProperty(test, 'responseText', {
  configurable: true,
  writable: true,
});

test.responseText = "Hey";

That doesn't work in Chrome, but this work in both Chrome and Firefox:

var test = new XMLHttpRequest();
var aValue;
Object.defineProperty(test, 'responseText', {
  get: function() { return aValue; },
  set: function(newValue) { aValue = newValue; },
  enumerable: true,
  configurable: true
});

test.responseText = "Hey";

The last was copy past from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

None of the solutions works in Safari. I tried to make a new XMLHttpRequest with writable properties, but it was not allowed to call open or send from it. I also tried this solution: https://mcmap.net/q/261096/-intercept-xmlhttprequest-and-modify-responsetext. Unfortunately it produced the same error in Safari:

TypeError: Attempting to configurable attribute of unconfigurable property.

Frivolity answered 18/5, 2015 at 21:45 Comment(1)
The delete method doesn't work in the newest Chrome version.Frivolity
K
1

First-class function variables are wonderful things! function f() {a; b; c; } is exactly the same thing as var f = function () {a; b; c; } This means you can redefine functions as needed. You want to wrap the function Mj to return a modified object? No problem. The fact that the responseText field is read-only is a pain, but if that's the only field you need...

var Mj_backup = Mj; // Keep a copy of it, unless you want to re-implement it (which you could do)
Mj = function (a, b, c, d, e) { // To wrap the old Mj function, we need its args
    var retval = Mj_backup(a,b,c,d,e); // Call the original function, and store its ret value
    var retmod; // This is the value you'll actually return. Not a true XHR, just mimics one
    retmod.responseText = retval.responseText; // Repeat for any other required properties
    return retmod;
}

Now, when your page code calls Mj(), it will invoke your wrapper instead (which will still call the original Mj internally, of course).

Knickerbocker answered 19/10, 2014 at 5:29 Comment(3)
If only it was that simple, but I forgot to include that the functions are not globally available, this was my mistake and I am sorry for that.Legitimate
Do you not have access to the scope in which they're defined? var Mj_backup = A.Fn.Mj; A.Fn.Mj = function... works just as well. I'm guessing there's some reason that doesn't work though, or you'd have done it...Knickerbocker
They are both contained inside an anonymous function (function(){})(); Like I said, if only it was that simple as warping the functions themselves instead of the XMLHttprequest.Legitimate
U
1

With the Proxy API, you can create a proxy for the XMLHttpRequest class and pollute the global window.XMLHttpRequest object.

"use strict"

window.XMLHttpRequest = class XMLHttpRequest {
  static _originalXMLHttpRequest = window.XMLHttpRequest

  constructor(...args) {
    this._XMLHttpRequestInstance = new XMLHttpRequest._originalXMLHttpRequest(...args)

    // If a return statement is used in a constructor with an object,
    // the object will be returned instead of `this`.
    // https://javascript.info/constructor-new#return-from-constructors
    return new Proxy(this, {
      get(instance, property) {
        if (property === "responseText") {
          // Modify the response string
          // `this` doesn't work inside an object, use `instance` instead
          return instance._XMLHttpRequestInstance.responseText.replace("some string", "another string")

          // Or return whatever you want
          return "whatever you wanted"
        }

        // Functions won't work without having `_XMLHttpRequestInstance` as `this`
        const value = instance._XMLHttpRequestInstance[property]
        return value instanceof Function ? value.bind(instance._XMLHttpRequestInstance) : value
      },
      set(instance, property, value) {
        // `this` doesn't work inside an object, use `instance` instead
        instance._XMLHttpRequestInstance[property] = value
        return true
      }
    })
  }
}

You can also modify addEventListener or other methods in the original XMLHttpRequest class.

return new Proxy(this, {
  get(instance, property) {
    // Modify listeners added by websites
    if (property === "addEventListener") {
      return (event, listener) => {
        if (event === "load") {
          instance._XMLHttpRequestInstance.addEventListener(event, () => {
            // Add your own code here
            // ...

            // In case you need to access the original listener
            listener()
          })
        } else {
          instance._XMLHttpRequestInstance.addEventListener(event, listener)
        }
      }
    }

    const value = instance._XMLHttpRequestInstance[property]
    return value instanceof Function ? value.bind(instance._XMLHttpRequestInstance) : value
  },
  set(instance, property, value) {
    // Remember to handle `xhr.onload = ...` too
    if (property === "onload") {
      instance._XMLHttpRequestInstance[property] = () => {
        // Add your own code here
        // ...

        // In case you need to access the original listener
        value()
      }
    } else {
      instance._XMLHttpRequestInstance[property] = value
    }

    return true
  }
})

See also: https://stackoverflow.com/a/65260802/

Uptodate answered 9/11, 2023 at 21:40 Comment(0)
A
-2

I search very a lot and made one solution to solve the problem

const open_prototype = XMLHttpRequest.prototype.open,
    intercept_response = function (urlpattern, callback) {
        XMLHttpRequest.prototype.open = function () {
            arguments['1'].includes(urlpattern) && this.addEventListener('readystatechange', function (event) {
                if (this.readyState === 4) {
                    var response = callback(event.target.responseText);
                    Object.defineProperty(this, 'response', {writable: true});
                    Object.defineProperty(this, 'responseText', {writable: true});
                    this.response = this.responseText = response;
                }
            });
            return open_prototype.apply(this, arguments);
        };
    };

and you can use the function like this

intercept_response('SOME_PART_OF_YOUR_API', (response) => {
    const new_response = response.replace('apple', 'orange');
    return new_response;
})

and now all apples replaced with oranges 😊

Altercation answered 8/7, 2022 at 8:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.