Shadow Shadow - 6 months ago 177
Ajax Question

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

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})();

Answer

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.