¿Cómo cancelar una solicitud de $ http en AngularJS?

190

Dada una solicitud de Ajax en AngularJS

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

cuál es la forma más efectiva de cancelar esa solicitud si se lanza otra solicitud (mismo backend, diferentes parámetros, por ejemplo).

mpm
fuente
8
Ninguna de las respuestas a continuación cancela la solicitud en sí. No hay forma de cancelar una solicitud HTTP una vez que abandona el navegador. Todas las respuestas a continuación simplemente abandonan al oyente de alguna manera. La solicitud HTTP aún llega al servidor, aún se procesa y el servidor aún enviará una respuesta, es solo un caso de si el cliente todavía está escuchando esa respuesta o no.
Liam
$ http solicitud cancelar en cambio de ruta freakyjolly.com/how-to-cancel-http-requests-in-angularjs-app
Code Spy
código para promise.abort() stackoverflow.com/a/50415480/984780
Luis Perez
@Liam mi pregunta no se cancelaba en el servidor. eso sería muy específico para la tecnología / implementación de su servidor. me preocupaba abandonar la devolución de llamada
Sonic Soul

Respuestas:

326

Esta característica se agregó a la versión 1.1.5 a través de un parámetro de tiempo de espera:

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
fuente
14
¿Qué debo hacer en caso de que necesite tanto un tiempo de espera como una cancelación manual por promesa?
Raman Chodźka
15
@ RamanChodźka Puedes hacer ambas cosas con una promesa; puede establecer un tiempo de espera para cancelar la promesa después de cierto tiempo, ya sea con la setTimeoutfunción nativa de JavaScript o con el $timeoutservicio de Angular .
Quinn Strahl
9
canceler.resolve () cancelará futuras solicitudes. Esta es una mejor solución: odetocode.com/blogs/scott/archive/2014/04/24/…
Toolkit
77
Otro buen ejemplo de una solución más completa de Ben Nadel: bennadel.com/blog/…
Pete
3
Realmente no funciona. ¿Podría proporcionar una muestra de trabajo?
Edward Olamisan
10

Cancelar Angular $ http Ajax con la propiedad de tiempo de espera no funciona en Angular 1.3.15. Para aquellos que no pueden esperar a que esto se solucione, estoy compartiendo una solución jQuery Ajax envuelta en Angular.

La solución involucra dos servicios:

  • HttpService (un contenedor alrededor de la función jQuery Ajax);
  • PendingRequestsService (rastrea las solicitudes de Ajax pendientes / abiertas)

Aquí va el servicio 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);

El servicio 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);

Más adelante en su servicio, cuando esté cargando datos, usaría el HttpService en lugar de $ http:

(function (angular) {

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

        this.getResources = function (params) {

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

        };
    }]);

})(window.angular);

Más adelante en su código le gustaría cargar los datos:

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

La cancelación de solicitudes emitidas con $httpno es compatible con la versión actual de AngularJS. Hay una solicitud de extracción abierta para agregar esta capacidad, pero este PR aún no se revisó, por lo que no está claro si va a llegar al núcleo de AngularJS.

pkozlowski.opensource
fuente
que PR fue rechazado, OP envió uno actualizado aquí github.com/angular/angular.js/pull/1836
Mark Nadig
Y eso también estaba cerrado.
frapontillo
Una versión de ella aterrizó como esta . Todavía estoy tratando de descubrir la sintaxis para usar la versión final. ¡Ojalá los RP vinieran con muestras de uso! :)
SimplGy
La página de documentación angular docs.angularjs.org/api/ng/service/$http en 'Uso' describe una configuración de tiempo de espera y también menciona qué objetos (una Promesa) se aceptan.
Igor Lino
6

Si desea cancelar las solicitudes pendientes en stateChangeStart con ui-router, puede usar algo como esto:

// en servicio

                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;

// en la configuración de 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
fuente
Esto funcionó para mí, muy simple y agregué otro para nombrar la llamada para poder seleccionar la llamada y solo cancelar algunas de las llamadas
Simon Dragsbæk
¿Por qué la configuración de UI Router necesita saber si request.timeoutestá presente?
trysis
6

Por alguna razón config.timeout no funciona para mí. Usé este enfoque:

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

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

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

Y cancelRequest.resolve () para cancelar. En realidad, no cancela una solicitud, pero al menos no recibe respuestas innecesarias.

Espero que esto ayude.

Aliaksandr Hmyrak
fuente
¿Viste tu SyntaxError { cancelPromise, httpPromise }?
Mephiztopheles
esta es la sintaxis de ES6, puede probar {c: cancelPromise, h: httpPromise}
Aliaksandr Hmyrak
Ya veo, inicializador de objetos
Mephiztopheles
3

Esto mejora la respuesta aceptada al decorar el servicio $ http con un método de cancelación de la siguiente manera ...

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

¿Qué está haciendo este código?

Para cancelar una solicitud, se debe establecer un tiempo de espera de "promesa". Si no se establece un tiempo de espera en la solicitud HTTP, el código agrega un tiempo de espera "prometedor". (Si ya se ha establecido un tiempo de espera, no se cambia nada).

Sin embargo, para resolver la promesa, necesitamos manejar el "diferido". Por lo tanto, utilizamos un mapa para poder recuperar el "diferido" más adelante. Cuando llamamos al método abortar, el "diferido" se recupera del mapa y luego llamamos al método resolver para cancelar la solicitud http.

Espero que esto ayude a alguien.

Limitaciones

Actualmente, esto solo funciona para $ http.get, pero puede agregar código para $ http.post, etc.

CÓMO UTILIZAR ...

Luego puede usarlo, por ejemplo, en el cambio de estado, de la siguiente manera ...

rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
  angular.forEach($http.pendingRequests, function (request) {
        $http.abort(request);
    });
  });
danday74
fuente
Estoy creando una aplicación que dispara algunas solicitudes http al mismo tiempo y necesito abortarlas manualmente. He probado su código pero solo aborta la última solicitud. ¿Te pasó eso antes? Cualquier ayuda sería apreciada.
Miguel Trabajo
1
el código aquí mantiene una búsqueda de referencias a los objetos diferidos para que puedan recuperarse más tarde, ya que el objeto diferido debe abortar. Lo importante con la búsqueda es la clave: par de valores. El valor es el objeto diferido. La clave es una cadena generada en función del método / url de solicitud. Supongo que está cancelando varias solicitudes al mismo método / url. Debido a esto, todas las teclas son idénticas y se sobrescriben entre sí en el mapa. Debe ajustar la lógica de generación de claves para que se genere una única, incluso si la url / método es la misma.
danday74
1
continúa desde arriba ... esto no es un error en el código, el código maneja la cancelación de múltiples solicitudes ... pero el código simplemente nunca tuvo la intención de cancelar varias solicitudes a la misma URL usando el mismo método http ... pero Si modifica la lógica, debería poder hacer que funcione con bastante facilidad.
danday74
1
¡Muchas gracias! ¡Estaba haciendo múltiples solicitudes a la misma URL pero con diferentes parámetros, y después de que dijiste sobre eso, cambié esa línea y funcionó de maravilla!
Miguel Trabajo
1

Aquí hay una versión que maneja múltiples solicitudes, también verifica el estado cancelado en la devolución de llamada para suprimir errores en el bloque de error. (en mecanografiado)

nivel de controlador:

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

en mi http obtener:

    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 de ayuda

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

Ahora, mirando la pestaña de red, veo que funciona de forma excelente. Llamé al método 4 veces y solo se realizó el último.

ingrese la descripción de la imagen aquí

Alma sónica
fuente
req.resolve ('cancelado'); no funciona para mí, estoy usando la versión 1.7.2. Incluso quiero cancelar una llamada si se vuelve a llamar y la primera llamada aún está en estado pendiente. por favor ayuda. siempre quiero proporcionar datos de llamadas recién canceladas cancelando todas las API pendientes de la misma URL
Sudarshan Kalebere
1

Puede agregar una función personalizada al $httpservicio utilizando un "decorador" que agregaría elabort() función a sus promesas.

Aquí hay un código de trabajo:

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 utiliza la funcionalidad de decorador de angularjs para agregar una with_abort()función al $httpservicio.

with_abort()utiliza la $httpopción de tiempo de espera que le permite cancelar una solicitud http.

La promesa devuelta se modifica para incluir una abort()función. También tiene código para asegurarse de que abort()funciona incluso si encadena promesas.

Aquí hay un ejemplo de cómo lo usarías:

// 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 defecto, cuando llama, abort()la solicitud se cancela y ninguno de los controladores de promesas se ejecuta.

Si desea que se llame a sus controladores de errores, pase true a abort(true).

En su controlador de errores, puede verificar si el "error" se debió a un "aborto" al verificar la xhrStatuspropiedad. Aquí hay un ejemplo:

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