How do I prevent a slow $http initiated in one route from potentially resolving after the user has changed routes?

StackOverflow https://stackoverflow.com//questions/24062940

Question

Let's say my current route is /books and I make an $http call to get all of the books we want to show a user. Normally, the call would resolve quickly and the books would be ng-repeated into the DOM. When we have an error, though (such as a timeout or there are no books returned), we update a common, global view that will overlay the content view and display a message like, "There are no books available." The common view is handled via a service with methods like CommonView.showLoading(), CommonView.showError("There are no books available."), and CommonView.hide(), etc.

Recently, I discovered that if the $http is not resolved quickly, the user may leave and go to another route (maybe /dinosaurs). Eventually, when the $http ends up resolving or being rejected, the promise call to display that common, global view will happen, resulting in an error view being displayed when there shouldn't be one, and the error will make no sense to the user (ie, user is at /dinosaurs and the error screen pops up with "There are no books available.").

I've seen that you can cancel an $http with a timeout promise, but this still seems like it could lead to race conditions (maybe you call cancel after processing of the resolve() or reject() has begun). I think it would be messy to have to check that the current route matches the route the $http was initiated from.

It seems like there should be some standard way to destroy $http calls on a route change or from a controller's $destroy method. I'd really like to avoid adding a lot of conditionals all over my gigantic app.

Was it helpful?

Solution

I can't find a great way to stop the processing of my callback if it's already started, but here's the $http wrapper I made to try and stop delayed callbacks from getting called after route changes. It doesn't replicate all of the $http methods, just the ones I needed. I haven't fully tested it, either. I've only verified that it will work in normal conditions (normal bandwidth with standard calls, ie httpWrapper.get(url).success(cb).error(err)). Your mileage may vary.

angular.module('httpWrapper', []).provider('httpWrapper', function() {
    this.$get = ['$rootScope','$http','$q', function($rootScope, $http, $q) {
        var $httpWrapper = function(config) {
        var deferred = $q.defer();
        var hasChangedRoute = false;
        var canceler = $q.defer();
        var http = null;
        var evListener = null;
        var promise = deferred.promise;

        if ((config || {}).timeout && typeof config.timeout === 'Object') {
            // timeout promise already exists
            canceler.promise = config.timeout;
        } else {
            angular.extend(config || {}, {
                timeout: canceler.promise
            });
        }
        http = $http(config)
            .success(function(data, status, headers, config) {
                // only call back if we haven't changed routes
                if (!hasChangedRoute) {
                    deferred.resolve({data:data, status:status, headers:headers, config:config});
                }
            })
            .error(function(data, status, headers, config) {
                // only call back if we haven't changed routes
                if (!hasChangedRoute) {
                    deferred.reject({data:data, status:status, headers:headers, config:config});
                }
            });

            evListener = $rootScope.$on('$locationChangeStart', function(scope, next, current) {
                hasChangedRoute = true;
                canceler.resolve('killing http');
                evListener(); // should unregister listener
            })

            promise.success = function(fn) {
                promise.then(function(response) {
                    fn(response.data, response.status, response.headers, config);
                });
                return promise;
            };
            promise.error = function(fn) {
                promise.then(null, function(response) {
                    fn(response.data, response.status, response.headers, config);
                });
                return promise;
            }
            return promise;
        };

        angular.forEach(['get', 'delete', 'head', 'jsonp'], function(method) {
            $httpWrapper[method] = function(url, config) {
                return $httpWrapper(
                    angular.extend(config || {}, {
                        method: method,
                        url: url
                    })
                );
            };
        });
        angular.forEach(['post', 'put'], function(method) {
            $httpWrapper[method] = function(url, data, config) {
                return $httpWrapper(
                    angular.extend(config || {}, {
                        method: method,
                        url: url,
                        data: data
                    })
                );
            };
        });
        return $httpWrapper;
    }];
});
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top