Aborting AJAX Requests Using $http And AngularJS

If you're coming from a jQuery background, you're probably used to calling .abort() on the AJAX (Asynchronous JavaScript and XML) response object (jqXHR). In AngularJS, things are a little bit more complicated. AngularJS don't expose the .abort() method; but, it does provide a way to abort an active AJAX request. It lets you define a promise object that will abort the underlying request if and when the promise value is resolved.

In AngularJS, you use the $http service to make AJAX requests. When you initiate the AJAX request, you have to provide $http with a configuration object. One of the configuration options is "timeout". This option takes either a numeric value (milliseconds) or a promise. If the promise is resolved before the AJAX request has completed, AngularJS will call .abort() on the underlying XMLHttpRequest object.

NOTE: The timeout property only supports milliseconds and promises as of v1.2. Prior to that, the timeout option only supported milliseconds; meaning, there was no way to manually abort the underlying XMLHttpRequest object, even in v1.0.8.

As of late, I've been trying to create better encapsulation of the data persistence and retrieval mechanisms. This makes the matter a bit more complex because I don't want the calling context to have to worry about the $http service or about wiring up the timeout. To accomplish this, I have to create the deferred object and then inject it into the data response promise before I return it to the calling context. This way, the calling context can just call .abort() on the promise and the AJAX request will be aborted.

To see what I mean, take a look at the following code. The API in this demo (not shown) has a 5-second sleep() call to give us enough time to abort the request.

<!doctype html> <html ng-app="Demo"> <head> <meta charset="utf-8" /> <title> Aborting AJAX Requests Using $http And AngularJS </title> <style type="text/css"> a[ ng-click ] { color: red ; cursor: pointer ; text-decoration: underline ; } </style> </head> <body ng-controller="DemoController"> <h1> Aborting AJAX Requests Using $http And AngularJS </h1> <p> <a ng-click="loadData()">Load Data</a> - <a ng-click="abortRequest()">Abort Request</a> </p> <!-- Show when data is loading. --> <p ng-if="isLoading"> <em>Loading...</em> </p> <!-- Show when data has finished loading. --> <ul ng-if="! isLoading"> <li ng-repeat="friend in friends"> {{ friend.name }} </li> </ul> <!-- Initialize scripts. --> <script type="text/javascript" src="../jquery/jquery-2.1.0.min.js"></script> <script type="text/javascript" src="../angularjs/angular-1.2.4.min.js"></script> <script type="text/javascript"> // Define the module for our AngularJS application. var app = angular.module( "Demo", [] ); // -------------------------------------------------- // // -------------------------------------------------- // // I control the main demo. app.controller( "DemoController", function( $scope, friendService ) { // I determine if remote data is currently being loaded. $scope.isLoading = false; // I contain the data that we wan to render. $scope.friends = []; // I hold the handle on the current request for data. Since we want to // be able to abort the request, mid-stream, we need to hold onto the // request which will have the .abort() method on it. var requestForFriends = null; // --- // PUBLIC METHODS. // --- // I abort the current request (if its running). $scope.abortRequest = function() { return( requestForFriends && requestForFriends.abort() ); }; // I load the remote data for the view. $scope.loadData = function() { // Flag the data is currently being loaded. $scope.isLoading = true; $scope.friends = []; // Make a request for data. Note that we are saving a reference to // this response rather than just piping it directly into a .then() // call. This is because we need to be able to access the .abort() // method on the request and we'll lose that original reference after // we call the .then() method. ( requestForFriends = friendService.getFriends() ).then( function( newFriends ) { // Flag the data as loaded. $scope.isLoading = false; $scope.friends = newFriends; }, function( errorMessage ) { // Flag the data as loaded (or rather, done trying to load). loading). $scope.isLoading = false; console.warn( "Request for friends was rejected." ); console.info( "Error:", errorMessage ); } ); }; } ); // -------------------------------------------------- // // -------------------------------------------------- // // I am the friend repository. app.service( "friendService", function( $http, $q ) { // I get the list of friends from the remote server. function getFriends() { // The timeout property of the http request takes a deferred value // that will abort the underying AJAX request if / when the deferred // value is resolved. var deferredAbort = $q.defer(); // Initiate the AJAX request. var request = $http({ method: "get", url: "./api/friends.cfm", timeout: deferredAbort.promise }); // Rather than returning the http-promise object, we want to pipe it // through another promise so that we can "unwrap" the response // without letting the http-transport mechansim leak out of the // service layer. var promise = request.then( function( response ) { return( response.data ); }, function( response ) { return( $q.reject( "Something went wrong" ) ); } ); // Now that we have the promise that we're going to return to the // calling context, let's augment it with the abort method. Since // the $http service uses a deferred value for the timeout, then // all we have to do here is resolve the value and AngularJS will // abort the underlying AJAX request. promise.abort = function() { deferredAbort.resolve(); }; // Since we're creating functions and passing them out of scope, // we're creating object references that may be hard to garbage // collect. As such, we can perform some clean-up once we know // that the requests has finished. promise.finally( function() { console.info( "Cleaning up object references." ); promise.abort = angular.noop; deferredAbort = request = promise = null; } ); return( promise ); } // Return the public API. return({ getFriends: getFriends }); } ); </script> </body> </html>

As you can see, after I've created the data-promise (as opposed to the AJAX-promise), I then augment it with an abort() method which will resolve the timeout.

The one big caveat to this approach is that if the calling context tries to invoke the .then() method on the resultant promise, the calling context will lose a handle on the .abort() method as it will be lost in the promise chain. This is why I am purposefully saving a reference to the primary promise before I hook into the resolution and rejection callbacks.

Aborting an AJAX request in AngularJS is not as easy as it is in jQuery. But, it's still possible; and, with enough encapsulation, you can still provide a simple interface for your data consumers. As a final note, I should also point out that $resource modules, as of v1.2, also provides a timeout option (though I have not yet tired it).

Tweet This Groovy post by @BenNadel - Aborting AJAX Requests Using $http And AngularJS Woot woot — you rock the party that rocks the body!







