196

Given a Ajax request in AngularJS

$http.get("/backend/").success(callback);

what is the most effective way to cancel that request if another request is launched (same backend, different parameters for instance).

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
mpm
  • 20,148
  • 7
  • 50
  • 55
  • 8
    None of the answers below actually cancel the request itself. There is no way to cancel a HTTP request once it leaves the browser. All the below answers simply abondon the listener in some way. The HTTP request still hits the server, is still processed and the server will still send a response, it's just a case of wether the client is still lisening for that response or not. – Liam Dec 14 '16 at 11:54
  • code for `promise.abort()` https://stackoverflow.com/a/50415480/984780 – Luis Perez May 18 '18 at 16:17
  • @Liam my question was not cancelling on the server. that would be very specific to what your server technology/implementation is. i was concerned with abandoning the callback – Sonic Soul Sep 17 '19 at 12:59

8 Answers8

332

This feature was added to the 1.1.5 release via a timeout parameter:

var canceler = $q.defer();
$http.get('/someUrl', {timeout: canceler.promise}).success(successCallback);
// later...
canceler.resolve();  // Aborts the $http request if it isn't finished.
lambinator
  • 10,616
  • 7
  • 55
  • 57
  • 14
    what should I do in case I need both a timeout and manual cancelling via promise? – Raman Chodźka Jul 26 '13 at 09:56
  • 15
    @RamanChodźka You can do both with a promise; you can set a timeout to cancel the promise after some amount of time, either with JavaScript's native `setTimeout` function or Angular's `$timeout` service. – Quinn Strahl Sep 17 '13 at 18:09
  • Thanks so much! My previous solution was just using jQuery for Ajax requests, and I really wanted to avoid that. – mirichan Feb 11 '14 at 08:25
  • 9
    canceler.resolve() will cancel future requests. This is a better solution: http://odetocode.com/blogs/scott/archive/2014/04/24/canceling-http-requests-in-angularjs.aspx – Toolkit Jul 27 '14 at 07:33
  • 7
    another good example of a more complete solution from Ben Nadel: http://www.bennadel.com/blog/2616-aborting-ajax-requests-using-http-and-angularjs.htm – Pete Jul 29 '14 at 12:34
  • 3
    Doesn't really work. Could you provide a working sample? – Edward Olamisan Mar 31 '15 at 13:59
  • 1
    Tried to implement this to cancel all pending requests on route change. All Pending requests are getting canceled But, still success callback (provided in controller) executing and console displaying errors (response is undefined) when we change route while there are some requests in pending status. (Those requests got canceled). – Jitendra Khatri Dec 03 '15 at 10:18
  • probably using `canceller.reject()` instead @JitendraKhatri? – Zorgatone May 05 '16 at 07:51
  • 1
    As i'm seeing in my network panel of dev tool of chrome, the download is still in progress even the resolve is called. – Sahasrangshu Guha Sep 19 '16 at 08:05
  • 2
    @Toolkit it will cancel future requests as it is already resolved, but if we do var canceler = $q.defer(); before every request, it would work. – gaurav5430 Jun 20 '17 at 11:36
  • For working code that lets you call `promise.abort()` checkout https://stackoverflow.com/a/50415480/984780 – Luis Perez May 18 '18 at 16:16
  • 1
    Confirming this method still works in late 2018. The bug in 1.3.x that some of the other answers/comment allude to must have been fixed. I started going on tangents while debugging the code thinking it might be angular that might be broken. In case anyone is thinking about going on same tangents, look more carefully at your code. This simple code works! – John Lee Dec 06 '18 at 19:51
  • anyone can please let me know which methods works for 1.7.2, to cancel particular pending api request? – Sudarshan Kalebere Sep 17 '19 at 12:04
  • @SudarshanKalebere I can confirm the above works for Angluar 1.7.8. The http request promise will reject and the error object will contain `xhrStatus: 'abort'` or something similar. – Huon Dec 05 '19 at 22:51
11

Cancelling Angular $http Ajax with the timeout property doesn't work in Angular 1.3.15. For those that cannot wait for this to be fixed I'm sharing a jQuery Ajax solution wrapped in Angular.

The solution involves two services:

  • HttpService (a wrapper around the jQuery Ajax function);
  • PendingRequestsService (tracks the pending/open Ajax requests)

Here goes the PendingRequestsService service:

    (function (angular) {
    'use strict';
    var app = angular.module('app');
    app.service('PendingRequestsService', ["$log", function ($log) {            
        var $this = this;
        var pending = [];
        $this.add = function (request) {
            pending.push(request);
        };
        $this.remove = function (request) {
            pending = _.filter(pending, function (p) {
                return p.url !== request;
            });
        };
        $this.cancelAll = function () {
            angular.forEach(pending, function (p) {
                p.xhr.abort();
                p.deferred.reject();
            });
            pending.length = 0;
        };
    }]);})(window.angular);

The HttpService service:

     (function (angular) {
        'use strict';
        var app = angular.module('app');
        app.service('HttpService', ['$http', '$q', "$log", 'PendingRequestsService', function ($http, $q, $log, pendingRequests) {
            this.post = function (url, params) {
                var deferred = $q.defer();
                var xhr = $.ASI.callMethod({
                    url: url,
                    data: params,
                    error: function() {
                        $log.log("ajax error");
                    }
                });
                pendingRequests.add({
                    url: url,
                    xhr: xhr,
                    deferred: deferred
                });            
                xhr.done(function (data, textStatus, jqXhr) {                                    
                        deferred.resolve(data);
                    })
                    .fail(function (jqXhr, textStatus, errorThrown) {
                        deferred.reject(errorThrown);
                    }).always(function (dataOrjqXhr, textStatus, jqXhrErrorThrown) {
                        //Once a request has failed or succeeded, remove it from the pending list
                        pendingRequests.remove(url);
                    });
                return deferred.promise;
            }
        }]);
    })(window.angular);

Later in your service when you are loading data you would use the HttpService instead of $http:

(function (angular) {

    angular.module('app').service('dataService', ["HttpService", function (httpService) {

        this.getResources = function (params) {

            return httpService.post('/serverMethod', { param: params });

        };
    }]);

})(window.angular);

Later in your code you would like to load the data:

(function (angular) {

var app = angular.module('app');

app.controller('YourController', ["DataService", "PendingRequestsService", function (httpService, pendingRequestsService) {

    dataService
    .getResources(params)
    .then(function (data) {    
    // do stuff    
    });    

    ...

    // later that day cancel requests    
    pendingRequestsService.cancelAll();
}]);

})(window.angular);
Edward Olamisan
  • 800
  • 1
  • 18
  • 28
9

Cancelation of requests issued with $http is not supported with the current version of AngularJS. There is a pull request opened to add this capability but this PR wasn't reviewed yet so it is not clear if its going to make it into AngularJS core.

pkozlowski.opensource
  • 117,202
  • 60
  • 326
  • 286
  • that PR was rejected, OP submitted updated one here https://github.com/angular/angular.js/pull/1836 – Mark Nadig Feb 05 '13 at 22:47
  • And that was closed as well. – frapontillo Apr 26 '13 at 09:58
  • A version of it [landed as this](https://github.com/angular/angular.js/commit/9f4f5937112655a9881d3281da8e72035bc8b180). Still trying to figure out the syntax to use the final version. Wish the PRs came with usage samples! :) – SimplGy Aug 13 '14 at 23:11
  • The angular documentation page https://docs.angularjs.org/api/ng/service/$http in the 'Usage' describes a timeout setting, and also mentions what objects (a Promise) are accepted. – Igor Lino Sep 22 '16 at 14:37
6

If you want to cancel pending requests on stateChangeStart with ui-router, you can use something like this:

// in service

                var deferred = $q.defer();
                var scope = this;
                $http.get(URL, {timeout : deferred.promise, cancel : deferred}).success(function(data){
                    //do something
                    deferred.resolve(dataUsage);
                }).error(function(){
                    deferred.reject();
                });
                return deferred.promise;

// in UIrouter config

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    //To cancel pending request when change state
       angular.forEach($http.pendingRequests, function(request) {
          if (request.cancel && request.timeout) {
             request.cancel.resolve();
          }
       });
    });
SonTL
  • 361
  • 4
  • 6
6

For some reason config.timeout doesn't work for me. I used this approach:

let cancelRequest = $q.defer();
let cancelPromise = cancelRequest.promise;

let httpPromise = $http.get(...);

$q.race({ cancelPromise, httpPromise })
    .then(function (result) {
...
});

And cancelRequest.resolve() to cancel. Actually it doesn't not cancel a request but you don't get unnecessary response at least.

Hope this helps.

3

This enhances the accepted answer by decorating the $http service with an abort method as follows ...

'use strict';
angular.module('admin')
  .config(["$provide", function ($provide) {

$provide.decorator('$http', ["$delegate", "$q", function ($delegate, $q) {
  var getFn = $delegate.get;
  var cancelerMap = {};

  function getCancelerKey(method, url) {
    var formattedMethod = method.toLowerCase();
    var formattedUrl = encodeURI(url).toLowerCase().split("?")[0];
    return formattedMethod + "~" + formattedUrl;
  }

  $delegate.get = function () {
    var cancelerKey, canceler, method;
    var args = [].slice.call(arguments);
    var url = args[0];
    var config = args[1] || {};
    if (config.timeout == null) {
      method = "GET";
      cancelerKey = getCancelerKey(method, url);
      canceler = $q.defer();
      cancelerMap[cancelerKey] = canceler;
      config.timeout = canceler.promise;
      args[1] = config;
    }
    return getFn.apply(null, args);
  };

  $delegate.abort = function (request) {
    console.log("aborting");
    var cancelerKey, canceler;
    cancelerKey = getCancelerKey(request.method, request.url);
    canceler = cancelerMap[cancelerKey];

    if (canceler != null) {
      console.log("aborting", cancelerKey);

      if (request.timeout != null && typeof request.timeout !== "number") {

        canceler.resolve();
        delete cancelerMap[cancelerKey];
      }
    }
  };

  return $delegate;
}]);
  }]);

WHAT IS THIS CODE DOING?

To cancel a request a "promise" timeout must be set. If no timeout is set on the HTTP request then the code adds a "promise" timeout. (If a timeout is set already then nothing is changed).

However, to resolve the promise we need a handle on the "deferred". We thus use a map so we can retrieve the "deferred" later. When we call the abort method, the "deferred" is retrieved from the map and then we call the resolve method to cancel the http request.

Hope this helps someone.

LIMITATIONS

Currently this only works for $http.get but you can add code for $http.post and so on

HOW TO USE ...

You can then use it, for example, on state change, as follows ...

rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
  angular.forEach($http.pendingRequests, function (request) {
        $http.abort(request);
    });
  });
danday74
  • 52,471
  • 49
  • 232
  • 283
  • I'm making an app that fires some http requests at the same time and I need to manualy abort them all. I've tried your code but it only aborts the last request. Did that happened to you before? Any help would be appreciated. – Miguel Trabajo Mar 31 '16 at 05:20
  • 1
    the code here maintains a lookup of references to the defer objects so that they can be retrieved later since the defer object is required to do an abort. the important thing with the lookup is the key:value pair. The value is the defer object. The key is a string generated based on the request method / url. I am guessing that you are aborting multiple requests to the same method / url. Because of this all the keys are identical and they overwrite one another in the map. You need to tweak the key generation logic so that a unique one is generated even if the url / method are the same. – danday74 Mar 31 '16 at 10:10
  • 1
    continued from above ... this is not a bug in the code, the code handles aborting multiple requests ... but the code was simply never meant to deal with aborting multiple requests to the same url using the same http method ... but if you tweak the logic you should be able to get it working fairly easily. – danday74 Mar 31 '16 at 10:11
  • 1
    Thank you very much! I was making multiple requests to the same url but with different parameters, and after you said about that I changed that line and it worked like a charm! – Miguel Trabajo Mar 31 '16 at 13:00
1

here is a version that handles multiple requests, also checks for cancelled status in callback to suppress errors in error block. (in Typescript)

controller level:

    requests = new Map<string, ng.IDeferred<{}>>();

in my http get:

    getSomething(): void {
        let url = '/api/someaction';
        this.cancel(url); // cancel if this url is in progress

        var req = this.$q.defer();
        this.requests.set(url, req);
        let config: ng.IRequestShortcutConfig = {
            params: { id: someId}
            , timeout: req.promise   // <--- promise to trigger cancellation
        };

        this.$http.post(url, this.getPayload(), config).then(
            promiseValue => this.updateEditor(promiseValue.data as IEditor),
            reason => {
                // if legitimate exception, show error in UI
                if (!this.isCancelled(req)) {
                    this.showError(url, reason)
                }
            },
        ).finally(() => { });
    }

helper methods

    cancel(url: string) {
        this.requests.forEach((req,key) => {
            if (key == url)
                req.resolve('cancelled');
        });
        this.requests.delete(url);
    }

    isCancelled(req: ng.IDeferred<{}>) {
        var p = req.promise as any; // as any because typings are missing $$state
        return p.$$state && p.$$state.value == 'cancelled';
    }

now looking at the network tab, i see that it works beatuifully. i called the method 4 times and only the last one went through.

enter image description here

Sonic Soul
  • 23,855
  • 37
  • 130
  • 196
  • req.resolve('cancelled'); is not working for me, i am using 1.7.2 version. Even i want to cancel a call if it is called again and first call is still in pending state. please help. i always want to provide newly called call data by canceling all pending api's of same url – Sudarshan Kalebere Sep 17 '19 at 10:58
1

You can add a custom function to the $http service using a "decorator" that would add the abort() function to your promises.

Here's some working code:

app.config(function($provide) {
    $provide.decorator('$http', function $logDecorator($delegate, $q) {
        $delegate.with_abort = function(options) {
            let abort_defer = $q.defer();
            let new_options = angular.copy(options);
            new_options.timeout = abort_defer.promise;
            let do_throw_error = false;

            let http_promise = $delegate(new_options).then(
                response => response, 
                error => {
                    if(do_throw_error) return $q.reject(error);
                    return $q(() => null); // prevent promise chain propagation
                });

            let real_then = http_promise.then;
            let then_function = function () { 
                return mod_promise(real_then.apply(this, arguments)); 
            };

            function mod_promise(promise) {
                promise.then = then_function;
                promise.abort = (do_throw_error_param = false) => {
                    do_throw_error = do_throw_error_param;
                    abort_defer.resolve();
                };
                return promise;
            }

            return mod_promise(http_promise);
        }

        return $delegate;
    });
});

This code uses angularjs's decorator functionality to add a with_abort() function to the $http service.

with_abort() uses $http timeout option that allows you to abort an http request.

The returned promise is modified to include an abort() function. It also has code to make sure that the abort() works even if you chain promises.

Here is an example of how you would use it:

// your original code
$http({ method: 'GET', url: '/names' }).then(names => {
    do_something(names));
});

// new code with ability to abort
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
    function(names) {
        do_something(names));
    });

promise.abort(); // if you want to abort

By default when you call abort() the request gets canceled and none of the promise handlers run.

If you want your error handlers to be called pass true to abort(true).

In your error handler you can check if the "error" was due to an "abort" by checking the xhrStatus property. Here's an example:

var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
    function(names) {
        do_something(names));
    }, 
    function(error) {
        if (er.xhrStatus === "abort") return;
    });
Luis Perez
  • 27,650
  • 10
  • 79
  • 80