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

前端 未结 7 1929
面向向阳花
面向向阳花 2020-12-01 04:11

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 \"

相关标签:
7条回答
  • 2020-12-01 04:17

    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;
    });
    
    0 讨论(0)
  • 2020-12-01 04:28

    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

    0 讨论(0)
  • 2020-12-01 04:28

    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.

    0 讨论(0)
  • 2020-12-01 04:32

    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://stackoverflow.com/a/28513219/3717718. Unfortunately it produced the same error in Safari:

    TypeError: Attempting to configurable attribute of unconfigurable property.

    0 讨论(0)
  • 2020-12-01 04:39

    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();
    
    0 讨论(0)
  • 2020-12-01 04:41

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

    0 讨论(0)
提交回复
热议问题