Add a "hook" to all AJAX requests on a page
Asked Answered
T

9

135

I'd like to know if it's possible to "hook" into every single AJAX request (either as it's about to get sent, or on events) and perform an action. At this point I'm assuming that there are other third-party scripts on the page. Some of these might use jQuery, while others do not. Is this possible?

Treatise answered 5/3, 2011 at 7:4 Comment(5)
It's possible with jQuery, so it's possible with plain old javascript, but you would need to have at least 2 "hooks" for each of them. Anyway, why use both on the same page?Gillyflower
How about using this library? github.com/slorber/ajax-interceptorSkimmia
Note: The answers to this question do not cover Ajax calls made from the newer fetch() API now in modern browsers.Benkley
How about the webRequest API?Straggle
@Straggle the webRequest API is a Firefox Browser Extension thing, not general-purpose Javascript that can run in the browser.Meet
M
122

Inspired by aviv's answer, I did a little investigating and this is what I came up with.
I'm not sure that it's all that useful as per the comments in the script and of course will only work for browsers using a native XMLHttpRequest object.
I think it will work if javascript libraries are in use as they will use the native object if possible.

function addXMLRequestCallback(callback){
    var oldSend, i;
    if( XMLHttpRequest.callbacks ) {
        // we've already overridden send() so just add the callback
        XMLHttpRequest.callbacks.push( callback );
    } else {
        // create a callback queue
        XMLHttpRequest.callbacks = [callback];
        // store the native send()
        oldSend = XMLHttpRequest.prototype.send;
        // override the native send()
        XMLHttpRequest.prototype.send = function(){
            // process the callback queue
            // the xhr instance is passed into each callback but seems pretty useless
            // you can't tell what its destination is or call abort() without an error
            // so only really good for logging that a request has happened
            // I could be wrong, I hope so...
            // EDIT: I suppose you could override the onreadystatechange handler though
            for( i = 0; i < XMLHttpRequest.callbacks.length; i++ ) {
                XMLHttpRequest.callbacks[i]( this );
            }
            // call the native send()
            oldSend.apply(this, arguments);
        }
    }
}

// e.g.
addXMLRequestCallback( function( xhr ) {
    console.log( xhr.responseText ); // (an empty string)
});
addXMLRequestCallback( function( xhr ) {
    console.dir( xhr ); // have a look if there is anything useful here
});
Madaras answered 5/3, 2011 at 9:47 Comment(6)
it would be nice to extend this response to support post-request hooksSkimmia
Based on your implementation, I've published something on NPM that works with both requests and responses! github.com/slorber/ajax-interceptorSkimmia
console.log( xhr.responseText ) is an empty string because at that moment xhr is empty. if you pass xhr variable to global variable and set delay few seconds, you will be able to access properties directlyAccidie
nice answer. If you want to get the response data, check the readyState and status when state changed. xhr.onreadystatechange=function(){ if ( xhr.readyState == 4 && xhr.status == 200 ) { console.log(JSON.parse(xhr.responseText)); } }Zoophilous
@ucheng If we add onreadystatechange of xhr, will it override the original ready state change method better use xhrObj.addEventListener("load", fn)Dymphia
@SebastienLorber if I understand correctly, it is possible to read and modify the requests but only read the responses. If we want to modify a response (not only in the hook), we need to use the WebExtension API: developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/…Couteau
B
178

NOTE: The accepted answer does not yield the actual response because it is getting called too early.

You can do this which will generically intercept any AJAX globally and not screw up any callbacks etc. that maybe have been assigned by any third party AJAX libraries.

(function() {
    var origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function() {
        console.log('request started!');
        this.addEventListener('load', function() {
            console.log('request completed!');
            console.log(this.readyState); //will always be 4 (ajax is completed successfully)
            console.log(this.responseText); //whatever the response was
        });
        origOpen.apply(this, arguments);
    };
})();

Some more docs of what you can do here with the addEventListener API here:

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress

(Note this doesn't work <= IE8)

Baryram answered 8/12, 2014 at 17:33 Comment(4)
Simply because I've searched a bit for this. The "load" event is only called on success. If you don't care about the result (just that the query did end) you can use the "loadend" eventDaze
You can use something like this.requestURL = arguments[1] to store the requestURL, which you can then retrieve in the event handler.Bertabertasi
Is it better to use prototype.open like in this answer or prototype.send like in meouw's accepted answer?Couteau
This answer is good but did not work with fetch().Eatton
M
122

Inspired by aviv's answer, I did a little investigating and this is what I came up with.
I'm not sure that it's all that useful as per the comments in the script and of course will only work for browsers using a native XMLHttpRequest object.
I think it will work if javascript libraries are in use as they will use the native object if possible.

function addXMLRequestCallback(callback){
    var oldSend, i;
    if( XMLHttpRequest.callbacks ) {
        // we've already overridden send() so just add the callback
        XMLHttpRequest.callbacks.push( callback );
    } else {
        // create a callback queue
        XMLHttpRequest.callbacks = [callback];
        // store the native send()
        oldSend = XMLHttpRequest.prototype.send;
        // override the native send()
        XMLHttpRequest.prototype.send = function(){
            // process the callback queue
            // the xhr instance is passed into each callback but seems pretty useless
            // you can't tell what its destination is or call abort() without an error
            // so only really good for logging that a request has happened
            // I could be wrong, I hope so...
            // EDIT: I suppose you could override the onreadystatechange handler though
            for( i = 0; i < XMLHttpRequest.callbacks.length; i++ ) {
                XMLHttpRequest.callbacks[i]( this );
            }
            // call the native send()
            oldSend.apply(this, arguments);
        }
    }
}

// e.g.
addXMLRequestCallback( function( xhr ) {
    console.log( xhr.responseText ); // (an empty string)
});
addXMLRequestCallback( function( xhr ) {
    console.dir( xhr ); // have a look if there is anything useful here
});
Madaras answered 5/3, 2011 at 9:47 Comment(6)
it would be nice to extend this response to support post-request hooksSkimmia
Based on your implementation, I've published something on NPM that works with both requests and responses! github.com/slorber/ajax-interceptorSkimmia
console.log( xhr.responseText ) is an empty string because at that moment xhr is empty. if you pass xhr variable to global variable and set delay few seconds, you will be able to access properties directlyAccidie
nice answer. If you want to get the response data, check the readyState and status when state changed. xhr.onreadystatechange=function(){ if ( xhr.readyState == 4 && xhr.status == 200 ) { console.log(JSON.parse(xhr.responseText)); } }Zoophilous
@ucheng If we add onreadystatechange of xhr, will it override the original ready state change method better use xhrObj.addEventListener("load", fn)Dymphia
@SebastienLorber if I understand correctly, it is possible to read and modify the requests but only read the responses. If we want to modify a response (not only in the hook), we need to use the WebExtension API: developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/…Couteau
G
22

Since you mention jquery, I know jquery offers a .ajaxSetup() method that sets global ajax options that include the event triggers like success, error, and beforeSend - which is what sounds like what you are looking for.

$.ajaxSetup({
    beforeSend: function() {
        //do stuff before request fires
    }
});

of course you would need to verify jQuery availability on any page you attempt to use this solution on.

Genesis answered 5/3, 2011 at 7:10 Comment(2)
Thanks for the suggestion, but this unfortunately does not intercept AJAX calls that are done not using AJAX.Treatise
Hugely helpful. Wanted to add, another trigger is statusCode. By plugging in something like statusCode: { 403: function() { error msg } you can provide a global auth/perms/role check back to all ajax functions without having to re-write a single .ajax request.Euhemerus
S
9

I've found a good library on Github that does the job well, you have to include it before any other js files

https://github.com/jpillora/xhook

here is an example that adds an http header to any incoming response

xhook.after(function(request, response) {
  response.headers['Foo'] = 'Bar';
});
Salley answered 8/9, 2016 at 9:35 Comment(4)
Great one! But didn't worked on Cordova application even with a crosswalk latest chrome web-view plugin!Cassiterite
Not working for all request, some fetch and xhr ok, but for example it does not catch xhr from google recaptchaUrchin
@SergioQuintero: I assume this is your GitHub issue: github.com/jpillora/xhook/issues/102. Consider deleting your comment here, since it was not a problem with the library.Smallsword
@SergioQuintero Any override of XHR interfaces only happens in that document, not globally in the browser because that would be a massive security/privacy hole. reCAPTCHA runs in an iframe and you can't run the override in there.Bellew
G
8

There is a trick to do it.

Before all scripts running, take the original XHMHttpReuqest object and save it in a different var. Then override the original XMLHttpRequest and direct all calls to it via your own object.

Psuedo code:

 var savd = XMLHttpRequest;
 XMLHttpRequest.prototype = function() {
     this.init = function() {
     }; // your code
     etc' etc'
 };
Goldfinch answered 5/3, 2011 at 7:9 Comment(1)
This answer isn't quite right, if you change the prototype of an object even the saved one will be changed. Also the entire prototype is being replaced with one function which will break all ajax requests. It did inspire me to offer an answer though.Madaras
D
8

Using the answer of "meouw" I suggest to use the following solution if you want to see results of request

function addXMLRequestCallback(callback) {
    var oldSend, i;
    if( XMLHttpRequest.callbacks ) {
        // we've already overridden send() so just add the callback
        XMLHttpRequest.callbacks.push( callback );
    } else {
        // create a callback queue
        XMLHttpRequest.callbacks = [callback];
        // store the native send()
        oldSend = XMLHttpRequest.prototype.send;
        // override the native send()
        XMLHttpRequest.prototype.send = function() {
            // call the native send()
            oldSend.apply(this, arguments);

            this.onreadystatechange = function ( progress ) {
               for( i = 0; i < XMLHttpRequest.callbacks.length; i++ ) {
                    XMLHttpRequest.callbacks[i]( progress );
                }
            };       
        }
    }
}

addXMLRequestCallback( function( progress ) {
    if (typeof progress.srcElement.responseText != 'undefined' &&                        progress.srcElement.responseText != '') {
        console.log( progress.srcElement.responseText.length );
    }
});
Dementia answered 12/11, 2013 at 11:34 Comment(0)
F
6

In addition to meouw's answer, I had to inject code into an iframe which intercepts XHR calls, and used the above answer. However, I had to change

XMLHttpRequest.prototype.send = function(){

To:

XMLHttpRequest.prototype.send = function(body)

And I had to change

oldSend.apply(this, arguments);

To:

oldSend.call(this, body);

This was necessary to get it working in IE9 with IE8 document mode. If this modification was not made, some call-backs generated by the component framework (Visual WebGUI) did not work. More info at these links:

Without these modifications AJAX postbacks did not terminate.

Fourflush answered 8/12, 2012 at 14:49 Comment(0)
M
3

jquery...

<script>
   $(document).ajaxSuccess(
        function(event, xhr, settings){ 
          alert(xhr.responseText);
        }
   );
</script>
Marelya answered 12/9, 2013 at 19:14 Comment(2)
jQuery will not catch requests made using other libraries. For example ExtJS. If you are only using jQuery, it's a good answer. Otherwise, it won't work all the time.Ellington
it wont even catch jquery requests if jquery instance is different. Change in protype if neededThegn
K
0

Check out jquery ajax events. You can do this:

$(document).on("ajaxSend", function (e) {
    console.log("before request is sent");
}).on("ajaxComplete", function (e) {
    console.log("after success or error");
}).on("ajaxSuccess ", function (e) {
    console.log("on success");
}).on("ajaxError ", function (e) {
    console.log("on error");
});
Kaspar answered 25/5, 2023 at 21:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.