OdeToCode IC Logo

Canceling $http Requests in AngularJS

Thursday, April 24, 2014

One of the objects you can pass along in the config argument of an $http operation is a timeout promise. If the promise resolves, Angular will cancel the corresponding HTTP request.

Sounds easy, but in practice there are a few complications. Before we get to the complications, let’s look at some easy code. Imagine the following  inside of a controller where a user can click a Cancel button.

var canceller = $q.defer();

$http.get("/api/movies/slow/2", { timeout: canceller.promise })
     .then(function(response){
        $scope.movie = response.data;
    });

$scope.cancel = function(){
    canceller.resolve("user cancelled");  
};

The code passes the canceller promise as the timeout option in the config object. If the user clicks cancel before the request completes, we’ll see the cancellation in the Network tab of the developer tools.

Cancelled HTTP Request

The complications come in real life scenarios where we have to manage multiple requests, provide the ability to cancel an operation to other client components, and figuring out if a given request is cancelled or not.

First, let’s look at a service that wraps $http to provide domain oriented operations. Typically services that talk using $http return simple promises, but now we need to return objects that provide a promise for the outstanding request, and  a method that can cancel the request.

app.factory("movies", function($http, $q){

    var getById = function(id){
        var canceller = $q.defer();

        var cancel = function(reason){
            canceller.resolve(reason);
        };

        var promise =
            $http.get("/api/movies/slow/" + id, { timeout: canceller.promise})
                .then(function(response){
                   return response.data;
                });

        return {
            promise: promise,
            cancel: cancel
        };
    };

    return {
        getById: getById
    };

});

A client of the service might need to track multiple requests, if there is a UI like the following that allows a user to send multiple requests.

<div ng-controller="mainController">

    <button ng-click="start()">
        Start Request
    </button>

    <ul>
        <li ng-repeat="request in requests">
            <button ng-click="cancel(request)">Cancel</button>
        </li>
    </ul>

    <ul>
        <li ng-repeat="m in movies">{{m.title}}</li>
    </ul>

</div>

The following code will manage the UI and allow the user to cancel any outstanding request.

app.controller("mainController", function($scope, movies) {

    $scope.movies = [];
    $scope.requests = [];
    $scope.id = 1;

    $scope.start = function(){

        var request = movies.getById($scope.id++);
        $scope.requests.push(request);
        request.promise.then(function(movie){
            $scope.movies.push(movie);
            clearRequest(request);
        }, function(reason){
            console.log(reason);
        });
    };

    $scope.cancel = function(request){
        request.cancel("User cancelled");
        clearRequest(request);
    };

    var clearRequest = function(request){
        $scope.requests.splice($scope.requests.indexOf(request), 1);
    };
});

The logic gets messy and could use some additional encapsulation to keep request management from overwhelming  the controller, but this is the essence of what you’d need to do to allow cancellation of $http operations.