How to cancel an $http request in AngularJS?

前端 未结 8 873
面向向阳花
面向向阳花 2020-11-22 09:49

Given a Ajax request in AngularJS

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

what is the most effective way to cancel that request if anot

相关标签:
8条回答
  • 2020-11-22 10:17

    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.

    0 讨论(0)
  • 2020-11-22 10:19

    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;
        });
    
    0 讨论(0)
  • 2020-11-22 10:25

    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.

    0 讨论(0)
  • 2020-11-22 10:27

    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);
        });
      });
    
    0 讨论(0)
  • 2020-11-22 10:29

    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);
    
    0 讨论(0)
  • 2020-11-22 10:32

    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.

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