53

Please go through the code first

app.js

var app = angular.module('Nimbus', ['ngRoute']);

route.js

app.config(function($routeProvider) {
    $routeProvider
    .when('/login', {
        controller: 'LoginController',
        templateUrl: 'templates/pages/login.html',
        title: 'Login'
    })
    .when('/home', {
        controller: 'HomeController',
        templateUrl: 'templates/pages/home.html',
        title: 'Dashboard'
    })
    .when('/stats', {
        controller: 'StatsController',
        templateUrl: 'templates/pages/stats.html',
        title: 'Stats'
    })
}).run( function($q, $rootScope, $location, $route, Auth) {
    $rootScope.$on( "$routeChangeStart", function(event, next, current) {
        console.log("Started");


        /* this line not working */
        var canceler = $q.defer();
        canceler.resolve();

    });

    $rootScope.$on("$routeChangeSuccess", function(currentRoute, previousRoute){
        $rootScope.title = ($route.current.title) ? $route.current.title : 'Welcome';
    });
 })

home-controller.js

app.controller('HomeController',
    function HomeController($scope, API) {
        API.all(function(response){
            console.log(response);
        })
    }
)

stats-controller.js

app.controller('StatsController',
    function StatsController($scope, API) {
        API.all(function(response){
            console.log(response);
        })
    }
)

api.js

app.factory('API', ['$q','$http', function($q, $http) {    
    return {
        all: function(callback) {
            var canceler = $q.defer();
            var apiurl = 'some_url'
            $http.get(apiurl,{timeout: canceler.promise}).success(callback);
        }
    }
}]);

When I move from home to stats , again API will send http request, I have many http calls like this, I pasted only few lines of code.

What I need is I need to cancel abort all pending http requests on routechangestart or success

Or any other way to implement the same ?

Miqdad Ali
  • 6,129
  • 7
  • 31
  • 50
  • When you say pending request you mean one that has been send to server but you haven't got response back? Or the one that is queued (due to concurrent http request limit for a single domain name)? – miensol Aug 23 '14 at 20:20
  • possible duplicate of [How to cancel an $http request in AngularJS?](http://stackoverflow.com/questions/13928057/how-to-cancel-an-http-request-in-angularjs) – jordiburgos Aug 24 '14 at 11:28
  • But way of http call is different and I dont think same we can apply here – Miqdad Ali Aug 25 '14 at 04:42

4 Answers4

55

I put together some conceptual code for this. It might need tweaking to fit your needs. There's a pendingRequests service that has an API for adding, getting and cancelling requests, a httpService that wraps $http and makes sure all requests are tracked.

By leveraging the $http config object (docs) we can get a way to cancel a pending request.

I've made a plnkr, but you're going to need quick fingers to see requests getting cancelled since the test-site I found typically responds within half a second, but you will see in the devtools network tab that requests do get cancelled. In your case, you would obviously trigger the cancelAll() call on the appropriate events from $routeProvider.

The controller is just there to demonstrate the concept.

DEMO

angular.module('app', [])
// This service keeps track of pending requests
.service('pendingRequests', function() {
  var pending = [];
  this.get = function() {
    return 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.canceller.resolve();
    });
    pending.length = 0;
  };
})
// This service wraps $http to make sure pending requests are tracked 
.service('httpService', ['$http', '$q', 'pendingRequests', function($http, $q, pendingRequests) {
  this.get = function(url) {
    var canceller = $q.defer();
    pendingRequests.add({
      url: url,
      canceller: canceller
    });
    //Request gets cancelled if the timeout-promise is resolved
    var requestPromise = $http.get(url, { timeout: canceller.promise });
    //Once a request has failed or succeeded, remove it from the pending list
    requestPromise.finally(function() {
      pendingRequests.remove(url);
    });
    return requestPromise;
  }
}])
// The controller just helps generate requests and keep a visual track of pending ones
.controller('AppCtrl', ['$scope', 'httpService', 'pendingRequests', function($scope, httpService, pendingRequests) {
  $scope.requests = [];
  $scope.$watch(function() {
    return pendingRequests.get();
  }, function(pending) {
    $scope.requests = pending;
  })

  var counter = 1;
  $scope.addRequests = function() {
    for (var i = 0, l = 9; i < l; i++) {
      httpService.get('https://public.opencpu.org/ocpu/library/?foo=' + counter++);  
    }
  };
  $scope.cancelAll = function() {
    pendingRequests.cancelAll();
  }
}]);
ivarni
  • 17,658
  • 17
  • 76
  • 92
  • [I got an answer to how to make it a near complete replacement here.](http://stackoverflow.com/questions/25625249/javascript-service-how-to-provide-a-method-like-an-object-constructor) – jmbmage Sep 02 '14 at 14:53
  • @jmb.mage Our http-wrapper does something similar, except it exposes `get`, `post`, `put` etc. and delegates to a single internal method that leverages the `$http` config-object. I just figured adding all that to my example would be noise since the context was how to abort requests :) Don't forget to to add the HTTP verb to the key in the pending list, and in the case of `POST` and `PUT` also a stringified version of the data. – ivarni Sep 03 '14 at 04:37
  • The setup is similar to this: https://stackoverflow.com/questions/22090792/cancelling-a-request-with-a-http-interceptor except that one does not deal with cancelling requests but rather preventing duplicate requests. – ivarni Sep 03 '14 at 04:40
  • Thanks for this - I'm surprised that this isn't a necessity for more people that are building spas. I had though that the best solution would be to stack requests up through a custom service, your code has pushed me more in that direction. – Ed Spencer May 21 '15 at 08:27
  • Excellent...Needed similar thing..only change I needed to do was to change "this.pending.length=0" to "pending.length=0".It was giving me undefined for "this.pending"... – Rips May 31 '15 at 19:04
  • @Rips Yup, that seems to be correct. Not sure how that snuck in there but `this.pending` will indeed cause an error. I'll edit the answer once I'm off this horribly slow airport WiFi, thanks for letting me know. – ivarni May 31 '15 at 19:59
  • 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). If I am doing promise.reject("Route Rejection"); then it calls error callback but requests does not gets aborted. – Jitendra Khatri Dec 04 '15 at 05:25
  • @ivarni This works great. Quick question, can we include a maximum timeout for requests? I want to cancel any particular request which doesn't respond in say 30seconds? – Srikanth Kondaparthy May 15 '16 at 23:57
  • 1
    @SrikanthKondaparthy I suppose you could further tweak this to add a timeout but I don't really work with angular any more so I don't know if there's any core functionality that can be used. The naive solution would be to use `setTimeout` to `resolve()` the canceller after 30sec (and remember to `clearTimeout` if it succeeds but there might be better solutions. Personally I think if there's a problem with requests taking 30 seconds then the main issue is on the server-side of things though :) – ivarni May 16 '16 at 09:20
  • @ivarni You are right. $timeout in combination with cancelAll will do the job. Thank you. – Srikanth Kondaparthy May 17 '16 at 02:37
20

You can use $http.pendingRequests to do that.

First, when you make request, do this:

var cancel = $q.defer();
var request = {
    method: method,
    url: requestUrl,
    data: data,
    timeout: cancel.promise, // cancel promise, standard thing in $http request
    cancel: cancel // this is where we do our magic
};

$http(request).then(.....);

Now, we cancel all our pending requests in $routeChangeStart

$rootScope.$on('$routeChangeStart', function (event, next, current) {

    $http.pendingRequests.forEach(function(request) {
        if (request.cancel) {
            request.cancel.resolve();
        }
    });
});

This way you can also 'protect' certain request from being cancelled by simply not providing 'cancel' field in request.

Frane Poljak
  • 2,315
  • 23
  • 25
  • This gets a handle to the deferred object underlying the promise and prematurely resolves it. This is not that same as "cancelling" the AJAX request itself, which will still process on the server and return. Not only will this prematurely trigger all the .then() handlers on the promise (probably causing null exceptions singe they are expecting a response object), I'd also expect it to throw a double resolution exception when the actual AJAX callback inevitably tries to resolve it again? – Eric Rini Feb 14 '17 at 20:47
  • Eric, I tested this and it works. If you look at the documentation (https://docs.angularjs.org/api/ng/service/$http), it states that 'timeout' property of an request can be a promise (timeout – {number|Promise} – timeout in milliseconds, or promise that should abort the request when resolved.). Property 'cancel' just gives us a power to do it ourselves, so when we resolve it on our own, the request acts like it reached a timeout. – Frane Poljak Feb 20 '17 at 18:19
  • Also, if you look at the answer with most +1's here, it does excatly the same thing, but is structurally nicer because it's wrapped up in a service. – Frane Poljak Feb 20 '17 at 18:21
  • 2
    Okay, I didn't realize exactly how the underlying implementation works. Adding a +1 for a minimal working solution. – Eric Rini Feb 21 '17 at 18:53
  • Fantastic, works even with $resource if you add your own interceptor to $httpProvider.interceptors which sets required fields to the outgoing request. – Mikhail May 05 '17 at 04:37
9

I think this is the best solution to abort requests. It's using an interceptor and $routeChangeSuccess event. http://blog.xebia.com/cancelling-http-requests-for-fun-and-profit/

Michael Clark
  • 462
  • 5
  • 17
rootatdarkstar
  • 1,486
  • 2
  • 15
  • 26
1

Please notice that im new with Angular so this may not be optimal. Another solution could be: on the $http request adding the "timeout" argument, Docs I did it this way:

In a factory where I call all my Rest services, have this logic.

module.factory('myactory', ['$http', '$q', function ($http, $q) {
    var canceler = $q.defer();

    var urlBase = '/api/blabla';
    var factory = {};

    factory.CANCEL_REQUESTS = function () {
        canceler.resolve();
        this.ENABLE_REQUESTS();
    };
    factory.ENABLE_REQUESTS = function () {
        canceler = $q.defer();
    };
    factory.myMethod = function () {
        return $http.get(urlBase, {timeout: canceler.promise});
    };
    factory.myOtherMethod= function () {
        return $http.post(urlBase, {a:a, b:b}, {timeout: canceler.promise});
    };
    return factory;
}]);

and on the angular app configuration I have:

return angular.module('app', ['ngRoute', 'ngSanitize', 'app.controllers', 'app.factories',
    'app.filters', 'app.directives', 'ui.bootstrap', 'ngGeolocation', 'ui.select' ])
.run(['$location', '$rootScope', 'myFactory', function($location, $rootScope, myFactory) {
    $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
        myFactory.CANCEL_REQUESTS();
        $rootScope.title = current.$$route.title;
    });
}]);

This way it catches all the "route" changes and stops all the request configured with that "timer" so you can select what is critical for you.

I hope it helps to someone. Regards

cesaregb
  • 765
  • 1
  • 11
  • 27