post-photo

How to write cancellable http requests with Angular $http service

Inspiration

The recent spread of the mobile internet connection almost completely changed the job profile of almost every frontend-developer. We steadily try to make our web pages responsive, and fearlessly fight against a wide variety of mobile browsers. But the mobile internet access has one more, a less freqently mentioned danger that causes sleepless nights for fronter developers.

Its speed is just incalculable.

Sometimes it works fine, but when it comes to bouncing along on the metro or just simply travelling in the counrtyside, it can become a disaster. On top of these, there is hardly any need to introduce the “always-running-out-of-broadband-data” syndrome. It might seem quite strange that I refer to this phenomenon as a complete disaster but slowness has some really strange side effects…

text

As if it were some ancient instinct, an overwhelming majority of people react to the extra slow internet connection with an extra big number of pointless clicking and tapping attempts. For a web-developer this means hundreds of untrackable asynchronous requests, and a massive allocation on the server. It was only a matter of time before one of our customers spoke up about this adverse situation and demanded a solution.

But what can a poor, confused junior developer offer in a sutiation like this?

All in all, we need to admit, that the essence of the problem are the unwanted, redundant requests. There is no use of handling good-for-nothing responses if the user changes his/her mind since the request was launched. A question arises as to whether we can cancel an action by manually tapping/clicking on a button? Can we manage this while not giving up the indisputably useful automatic timeout functionality? I think, this is the time for moving contemptation to action and make an attempt to find a solution for this problem.

Javascript promises as $http timeout parameter

When I am talking about Angular $http service and timeout parameter, everybody remembers the good old solution with a simple number in the parameter list of the http request:

var sendHttpGetRequest = function (url, param1, param2) {
   var params = {param:param1, param2:param2};
 
   return $http.get(url,
      {
         timeout: 20000,
         params: params
      }
   );
};

But after some further investigation in the Angular documentation of $http service, I stumbled into a very interesting sentece:

It’s failure that gives you the proper perspective on success

Soon I decided to overwrite my http requests by using my newly acquired skills and use a javascript promise as an http timeout parameter. I bounded the cancellation of the promise to a click event of a button. Moreover, I set a countdown right after sending each http request by using Javascript the timeout method. After waiting a specified number of milliseconds, the promise became rejected automatically. The request in my service looked like this:

var cancelRetrieveDoc = function () {
   canceller.resolve('manually-cancelled');
};
var sendHttpGetRequest = function (url, param1, param2) {
    var params = {param:param1, param2:param2};
    var canceller = new Promise();
  
    $timeout(function () {
         canceller.resolve('automatically-cancelled');
    }, 20000);
   return $http.get(url,
      {
         timeout: canceller,
         params: params
      }
   );
};

When it came to testing, my first impressions were encouragingly good. The http request cancellation worked like a charm! Unfortunately, all my expectations fell apart soon. The automatic timeout begun to behave unpredictably, cancelling the request in random moments. This behavior seemed to be just incalculabe. For hours and hours I hunted for the reasons, but everything seemed to work perfectly except for these random cancellations. It took me quite a while to see the reason of the bad behavior:

Represeting these events on a timeline, the reason of the random cancellation becomes even clearer:

text

The aims are now clear: every request needs its own canceller function and a promise for a timeout parameter. Though the implementation is a bit tricky in Angular, in the next paragraph I will show you how I solved this issue by a simple example.

The yearbook

Let’s imagine that you are asked to make an online yearbook for your classmates, who are graduating from secondary school. The application contains two states. A main list state, containing the list of the students, and a profile state, that contains the detailed profile of a given student.

text text

Every state has a factory and a service. All the data is saved temporarly in javascript variables in factories. If a controller needs any data, it asks the factory for it. If it is not stored in the factory, the factory calls the proper function in the service, that sends the http request for the server.

The profile state is a child state of the main list state, and can be reached by clicking on a given student’s picture. The app does not navigate to the given profile till the data of the chosen student is not loaded. The aim is to be able to launch multiple pararell requests, and cancel the ones you don’t need any more. The following illustration summarized the aforsaid architecture:

text

The process we need to concentrate on is highlighted with red. If the user clicks on a picture in the list, the ListController asks the given student’s data from DetailsFactory. If the data is not already stored, the DetailsSercvice will send the http request.

And here comes th big trick. For every single request we need a new promise, and a new canceller function. These three should belong together and be a member of a so-called RequestHandler function. In case of a new request we need to instantiate a new RequestHandler in the DetailsFactory and also in the DetailsService.

All we need to do is to perform the following changes to the code:

var cancellableApp = angular.module('cancellableApp');
     
    cancellableApp.controller('ListController', ['$scope', '$q', '$filter', '$state', '$timeout', 'ListFactory', 'DetailsFactory', function ($scope, $q, $filter, $state, $timeout, ListFactory, DetailsFactory) {
    var vm = this;
        vm.docs = {};
        vm.requests = [];
 
        vm.members = [...];
 
        vm.goToSelectedDoc = function (index) {
            vm.requests[index] = {};
            vm.requests[index].status = 'loading';
            vm.requests[index].dochandler = DetailsFactory.getDocHandler();
            vm.requests[index].dochandler.getDoc(index).then(function () {
                vm.requests[index].status = 'done';
                // $state.go('list.details', {ID: index});
            }).catch(function () {
                vm.requests[index] = null;
            });
        };
 
        vm.cancelRetrieveDoc = function(index) {
            vm.requests[index].dochandler.cancelretrieveDoc();
        };
 
        vm.clickOnDimming = function(index) {
            if (vm.requests[index].status == 'done'){
                $state.go('list.details', {index: index});
            } else if (vm.requests[index].status == 'loading') {
                vm.cancelRetrieveDoc(index);
            }
        };
    }]);
var cancellableApp = angular.module('cancellableApp');
     
cancellableApp.factory('DetailsFactory', ['DetailsService','$q','$stateParams', function (detailsService, $q, $stateParams) {
 
        var detailsFactory = {};
        detailsFactory.docData= [];
 
 
        detailsFactory.getDocHandler = function() {
            var docHandler = detailsService.getDocHandler();
 
            var getDoc = function (index) {
                var deferred = $q.defer();
 
                docHandler.retrieveDoc(index).then(function (response) {
                    detailsFactory.docData[index] = response.data[index];
                    deferred.resolve();
                }, function() {
                    deferred.reject();
                });
                return deferred.promise;
            };
 
            var cancelretrieveDoc = function() {
                docHandler.cancelRetrieveDoc();
            };
            return {
                getDoc:getDoc,
                cancelretrieveDoc:cancelretrieveDoc
            }
        };
 
        return detailsFactory;
    }]);
var cancellableApp = angular.module('cancellableApp');
     
cancellableApp.factory('DetailsService', ['$q', '$http', '$timeout', 'RequestHelper', function ($q, $http, $timeout, RequestHelper) {
 
        var detailsService = {};
 
        detailsService.getDocHandler = function () {
 
            var canceller = $q.defer();
 
            var cancelRetrieveDoc = function () {
                canceller.resolve('manually-cancelled');
            };
 
            var retrieveDoc = function (id) {
                return sendHttpRequestGetDoc(id);
            };
 
            var sendHttpRequestGetDoc = function (id) {
                var url = "test.json";
                var params = RequestHelper.getDocumentParameters(id);
 
                $timeout(function () {
                    canceller.resolve('automatically-cancelled');
                }, 20000);
 
                return $http.get(url,
                    {
                        timeout: canceller.promise,
                        params: params
                    }
                );
            };
            return {
                retrieveDoc: retrieveDoc,
                cancelRetrieveDoc: cancelRetrieveDoc
            }
        };
        return detailsService;
    }]);

Combined this with some additional css tricks, our webapp will take its final shape: text

Feel free to check it out in action in the following plunker:

https://plnkr.co/edit/7nqx9g?p=info

If you would like to test the request cancellation by a simulating slow Internet connection, follow the next instructions (Chrome):

All in all

I hope I could provide some useful advice to fix the horrendeous clicking habits of your users. If you have any suggestions on this topic, I’d be glad to converse!

member photo

She is into working with Angular 2 on frontends and Scala on backends. (And anything else that helps her make beautifully sleek web apps.)

Latest post by Csenge Sóti

Monday Morning with a Content-Security-Policy HTTP Response Header