132

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?

Yuliy
  • 17,381
  • 6
  • 41
  • 47
  • 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? – yoda Mar 05 '11 at 07:09
  • 2
    How about using this library? https://github.com/slorber/ajax-interceptor – Sebastien Lorber Sep 19 '14 at 15:23
  • 3
    Note: The answers to this question do not cover Ajax calls made from the newer `fetch()` API now in modern browsers. – jfriend00 Jun 23 '17 at 19:40
  • How about the [`webRequest` API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Intercept_HTTP_requests)? – ggorlen Apr 12 '22 at 13:46
  • @ggorlen the webRequest API is a Firefox Browser Extension thing, not general-purpose Javascript that can run in the browser. – Matthew Marichiba Jan 13 '23 at 08:39

9 Answers9

172

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)

John Culviner
  • 22,235
  • 6
  • 55
  • 51
  • 2
    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"` event – Romuald Brunet Mar 29 '17 at 16:34
  • You can use something like `this.requestURL = arguments[1]` to store the requestURL, which you can then retrieve in the event handler. – Marcus Adams Aug 20 '20 at 16:35
  • Is it better to use `prototype.open` like in this answer or `prototype.send` like in meouw's accepted answer? – baptx Dec 05 '20 at 23:27
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
});
Community
  • 1
  • 1
meouw
  • 41,754
  • 10
  • 52
  • 69
  • it would be nice to extend this response to support post-request hooks – Sebastien Lorber Sep 19 '14 at 13:45
  • 1
    Based on your implementation, I've published something on NPM that works with both requests and responses! https://github.com/slorber/ajax-interceptor – Sebastien Lorber Sep 19 '14 at 15:21
  • 4
    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 directly – Toolkit Mar 09 '15 at 07:56
  • 5
    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)); } } – Stanley Wang Feb 16 '16 at 21:50
  • 2
    @ucheng If we add onreadystatechange of xhr, will it override the original ready state change method better use xhrObj.addEventListener("load", fn) – Mohamed Hussain Aug 07 '17 at 09:43
  • @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: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Intercept_HTTP_requests – baptx Dec 06 '20 at 12:11
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.

jondavidjohn
  • 61,812
  • 21
  • 118
  • 158
  • 1
    Thanks for the suggestion, but this unfortunately does not intercept AJAX calls that are done not using AJAX. – Yuliy Mar 05 '11 at 23:27
  • 2
    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. – Watercayman May 18 '16 at 19:51
10

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';
});
Mohamed Ali
  • 3,717
  • 1
  • 33
  • 39
  • 2
    Great one! But didn't worked on Cordova application even with a crosswalk latest chrome web-view plugin! – Islam Attrash Nov 27 '16 at 09:54
  • Not working for all request, some fetch and xhr ok, but for example it does not catch xhr from google recaptcha – Sergio Quintero May 07 '19 at 16:28
  • @SergioQuintero: I assume this is your GitHub issue: https://github.com/jpillora/xhook/issues/102. Consider deleting your comment here, since it was not a problem with the library. – thirtydot Jun 22 '19 at 17:40
  • @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. – Walf Mar 02 '20 at 00:01
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 );
    }
});
heroin
  • 2,148
  • 1
  • 23
  • 32
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'
 };
Arasuvel
  • 2,971
  • 1
  • 25
  • 40
aviv
  • 2,719
  • 7
  • 35
  • 48
  • 10
    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. – meouw Mar 05 '11 at 09:59
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.

Walf
  • 8,535
  • 2
  • 44
  • 59
3

jquery...

<script>
   $(document).ajaxSuccess(
        function(event, xhr, settings){ 
          alert(xhr.responseText);
        }
   );
</script>
  • 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. – npiani Sep 04 '14 at 13:44
  • 1
    it wont even catch jquery requests if jquery instance is different. Change in protype if needed – Shishir Arora Aug 27 '15 at 21:04
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");
});