Como cancelar uma solicitação de $ http no AngularJS?

189

Dada uma solicitação do Ajax no AngularJS

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

qual é a maneira mais eficaz de cancelar essa solicitação se outra solicitação for lançada (mesmo back-end, parâmetros diferentes, por exemplo).

mpm
fonte
8
Nenhuma das respostas abaixo realmente cancela a própria solicitação. Não há como cancelar uma solicitação HTTP depois que ela sai do navegador. Todas as respostas abaixo simplesmente abandonam o ouvinte de alguma forma. A solicitação HTTP ainda atinge o servidor, ainda é processada e o servidor ainda envia uma resposta, é apenas um caso de o cliente ainda estar aguardando por essa resposta ou não.
Liam
$ http request cancel on change change freakyjolly.com/how-to-cancel-http-requests-in-angularjs-app
Código Spy
código para promise.abort() stackoverflow.com/a/50415480/984780
Luis Perez
@ Liam minha pergunta não foi cancelada no servidor. isso seria muito específico para qual é a sua tecnologia / implementação de servidor. eu estava preocupado em abandonar o retorno de chamada #
Sonic Soul

Respostas:

325

Este recurso foi adicionado à versão 1.1.5 por meio de um parâmetro de tempo limite:

var canceler = $q.defer();
$http.get('/someUrl', {timeout: canceler.promise}).success(successCallback);
// later...
canceler.resolve();  // Aborts the $http request if it isn't finished.
lambinador
fonte
13
o que devo fazer caso precise de um tempo limite e o cancelamento manual por promessa?
Raman Chodźka
15
@ RamanChodźka Você pode fazer as duas coisas com uma promessa; você pode definir um tempo limite para cancelar a promessa após um certo período de tempo, com a setTimeoutfunção nativa do JavaScript ou o $timeoutserviço da Angular .
Quinn Strahl
9
canceler.resolve () cancelará solicitações futuras. Esta é uma solução melhor: odetocode.com/blogs/scott/archive/2014/04/24/…
Toolkit
7
outro bom exemplo de uma solução mais completa de Ben Nadel: bennadel.com/blog/…
Pete
3
Realmente não funciona. Você poderia fornecer uma amostra de trabalho?
Edward Olamisan
10

O cancelamento do Angular $ http Ajax com a propriedade timeout não funciona no Angular 1.3.15. Para aqueles que não podem esperar que isso seja corrigido, estou compartilhando uma solução jQuery Ajax envolvida em Angular.

A solução envolve dois serviços:

  • HttpService (um wrapper em torno da função jQuery Ajax);
  • PendingRequestsService (rastreia as solicitações Ajax pendentes / abertas)

Aqui está o serviço PendingRequestsService:

    (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);

O serviço HttpService:

     (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);

Posteriormente em seu serviço, ao carregar dados, você usaria o HttpService em vez de $ http:

(function (angular) {

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

        this.getResources = function (params) {

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

        };
    }]);

})(window.angular);

Posteriormente no seu código, você gostaria de carregar os dados:

(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
fonte
9

O cancelamento de solicitações emitidas $httpnão é suportado na versão atual do AngularJS. Há uma solicitação de recebimento aberta para adicionar esse recurso, mas esse PR ainda não foi revisado, portanto, não está claro se ele fará parte do núcleo do AngularJS.

pkozlowski.opensource
fonte
que o PR foi rejeitado, OP apresentaram atualizadas um aqui github.com/angular/angular.js/pull/1836
Mark Nadig
E isso foi fechado também.
Frapontillo
Uma versão dele caiu assim . Ainda estou tentando descobrir a sintaxe para usar a versão final. Gostaria que os PRs viessem com amostras de uso! :)
SimplGy 13/08
A página de documentação angular docs.angularjs.org/api/ng/service/$http em 'Uso' descreve uma configuração de tempo limite e também menciona quais objetos (uma Promessa) são aceitos.
Igor Lino
6

Se você deseja cancelar solicitações pendentes no stateChangeStart com o ui-router, você pode usar algo como isto:

// em serviço

                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;

// na configuração do UIrouter

$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
fonte
Isso funcionou para mim - muito simples e eu adicionei um outro para nomear a chamada para que eu possa selecionar a chamada e só cancelar algumas das chamadas
Simon Dragsbæk
Por que a configuração do roteador da interface do usuário precisa saber se request.timeoutestá presente?
trysis
6

Por alguma razão, config.timeout não funciona para mim. Eu usei essa abordagem:

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

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

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

E cancelRequest.resolve () para cancelar. Na verdade, ele não cancela uma solicitação, mas você não recebe respostas desnecessárias pelo menos.

Espero que isto ajude.

Aliaksandr Hmyrak
fonte
Você viu seu SyntaxError { cancelPromise, httpPromise }?
Mephiztopheles
esta é a sintaxe do ES6, você pode tentar {c: cancelPromise, h: httpPromise} #
Aliaksandr Hmyrak
Vejo, objeto shortinitializer
Mephiztopheles
3

Isso aprimora a resposta aceita decorando o serviço $ http com um método de cancelamento da seguinte maneira ...

'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;
}]);
  }]);

O QUE ESTE CÓDIGO ESTÁ FAZENDO?

Para cancelar uma solicitação, um tempo limite de "promessa" deve ser definido. Se nenhum tempo limite for definido na solicitação HTTP, o código adicionará um tempo limite de "promessa". (Se um tempo limite já estiver definido, nada será alterado).

No entanto, para resolver a promessa, precisamos lidar com o "adiado". Assim, usamos um mapa para recuperar o "adiado" posteriormente. Quando chamamos o método abort, o "adiado" é recuperado do mapa e, em seguida, chamamos o método resolve para cancelar a solicitação http.

Espero que isso ajude alguém.

LIMITAÇÕES

Atualmente, isso funciona apenas para $ http.get, mas você pode adicionar código para $ http.post e assim por diante

COMO USAR ...

Você pode usá-lo, por exemplo, na mudança de estado, da seguinte maneira ...

rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
  angular.forEach($http.pendingRequests, function (request) {
        $http.abort(request);
    });
  });
danday74
fonte
Estou criando um aplicativo que dispara algumas solicitações http ao mesmo tempo e preciso anular todas manualmente. Eu tentei seu código, mas ele anula apenas a última solicitação. Isso já aconteceu com você antes? Qualquer ajuda seria apreciada.
Miguel Trabajo 31/03
1
o código aqui mantém uma pesquisa de referências aos objetos adiados, para que possam ser recuperados posteriormente, pois é necessário que o objeto adiado faça um cancelamento. o importante com a pesquisa é a chave: par de valores. O valor é o objeto de adiamento. A chave é uma string gerada com base no método / URL da solicitação. Suponho que você esteja cancelando várias solicitações para o mesmo método / URL. Por esse motivo, todas as chaves são idênticas e se substituem no mapa. Você precisa ajustar a lógica de geração de chaves para que uma única seja gerada, mesmo que o URL / método seja o mesmo.
danday74
1
continuado de cima ... isso não é um bug no código, o código lida com o cancelamento de várias solicitações ... mas o código nunca foi feito para lidar com o cancelamento de várias solicitações para o mesmo URL usando o mesmo método http ... se você ajustar a lógica, poderá fazê-la funcionar com bastante facilidade.
danday74
1
Muito obrigado! Eu estava fazendo várias solicitações no mesmo URL, mas com parâmetros diferentes, e depois que você falou sobre isso, mudei essa linha e funcionou como um encanto!
Miguel Trabajo 31/03
1

Aqui está uma versão que lida com várias solicitações, também verifica o status cancelado no retorno de chamada para suprimir erros no bloco de erros. (em texto datilografado)

nível do controlador:

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

no meu 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(() => { });
    }

métodos auxiliares

    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';
    }

Agora, olhando para a guia rede, vejo que funciona muito bem. chamei o método 4 vezes e apenas o último passou.

insira a descrição da imagem aqui

Sonic Soul
fonte
req.resolve ('cancelado'); não está funcionando para mim, estou usando a versão 1.7.2. Mesmo eu quero cancelar uma chamada se ela for chamada novamente e a primeira chamada ainda estiver no estado pendente. por favor ajude. Eu sempre quero fornecer dados de chamadas recém-chamados anulando a totalidade api pendente é de mesma url
Sudarshan Kalebere
1

Você pode adicionar uma função personalizada ao $httpserviço usando um "decorador" que adiciona a abort()função às suas promessas.

Aqui está um código de trabalho:

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;
    });
});

Este código usa a funcionalidade do decorador do angularjs para adicionar uma with_abort()função ao $httpserviço.

with_abort()usa a $httpopção de tempo limite que permite anular uma solicitação http.

A promessa retornada é modificada para incluir uma abort()função. Ele também possui código para garantir que abort()funcione, mesmo que você prometa em cadeia.

Aqui está um exemplo de como você o usaria:

// 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

Por padrão, quando você chama, abort()a solicitação é cancelada e nenhum dos manipuladores de promessa é executado.

Se você deseja que seus manipuladores de erro sejam chamados, passe para true abort(true).

No manipulador de erros, você pode verificar se o "erro" ocorreu devido a um "cancelamento", verificando a xhrStatuspropriedade Aqui está um exemplo:

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