How to cancel an $http request in AngularJS?
Asked Answered
S

8

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).

Slaughter answered 18/12, 2012 at 7:12 Comment(3)
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.Kop
code for promise.abort() https://mcmap.net/q/127921/-how-to-cancel-an-http-request-in-angularjsDaryldaryle
@Kop 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 callbackVoluptuary
D
331

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.
Didymous answered 26/6, 2013 at 19:0 Comment(14)
what should I do in case I need both a timeout and manual cancelling via promise?Fimbriate
@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.Norite
Thanks so much! My previous solution was just using jQuery for Ajax requests, and I really wanted to avoid that.Quigley
canceler.resolve() will cancel future requests. This is a better solution: odetocode.com/blogs/scott/archive/2014/04/24/…Fulfill
another good example of a more complete solution from Ben Nadel: bennadel.com/blog/…Endpaper
Doesn't really work. Could you provide a working sample?Mackinaw
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).Cusp
probably using canceller.reject() instead @JitendraKhatri?Napper
As i'm seeing in my network panel of dev tool of chrome, the download is still in progress even the resolve is called.Chlo
@Fulfill it will cancel future requests as it is already resolved, but if we do var canceler = $q.defer(); before every request, it would work.Aphaeresis
For working code that lets you call promise.abort() checkout https://mcmap.net/q/127921/-how-to-cancel-an-http-request-in-angularjsDaryldaryle
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!Mikey
anyone can please let me know which methods works for 1.7.2, to cancel particular pending api request?Sender
@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.Wealth
M
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);
Mackinaw answered 1/4, 2015 at 19:21 Comment(0)
D
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.

Deepsix answered 30/12, 2012 at 16:35 Comment(4)
that PR was rejected, OP submitted updated one here github.com/angular/angular.js/pull/1836Fanchan
And that was closed as well.Illogical
A version of it landed as this. Still trying to figure out the syntax to use the final version. Wish the PRs came with usage samples! :)Natala
The angular documentation page docs.angularjs.org/api/ng/service/$http in the 'Usage' describes a timeout setting, and also mentions what objects (a Promise) are accepted.Rosenblum
W
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();
          }
       });
    });
Whew answered 1/7, 2015 at 7:33 Comment(2)
This worked for me - very simple and i added another one to name the call so i can select the call and only cancel some of the callsEarwitness
Why does the UI Router config need to know if request.timeout is present?Boeotia
F
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.

Fermentative answered 13/12, 2016 at 20:20 Comment(3)
Did you see your SyntaxError { cancelPromise, httpPromise }?Marcenemarcescent
this is ES6 syntax, you can try { c: cancelPromise, h: httpPromise }Fermentative
I see, object shortinitializerMarcenemarcescent
S
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);
    });
  });
Slapup answered 5/10, 2015 at 12:19 Comment(4)
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.Gao
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.Slapup
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.Slapup
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!Gao
V
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

Voluptuary answered 29/11, 2017 at 20:28 Comment(1)
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 urlSender
D
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;
    });
Daryldaryle answered 18/5, 2018 at 16:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.