HTTP Retry Service in AngularJS

By

I recently found myself in a situation where I had to retry sending a failed HTTP request an arbitrary number of times before returning an error. This particular project was built using Angular, so I decided to create a simple service to solve this problem. By doing so I could avoid getting caught in a nightmare of callbacks, and stay true to the DRY principle; since I could reuse this service in other controllers and services.

Setting up our service

Let’s start by creating a basic skeleton for our new service:

(function() {
	'use strict';

	angular.module('myAwesomeApp').factory('HttpRetryService', HttpRetryService);

	function HttpRetryService() {
		var httpRetryService = {};
		return httpRetryService;
	}
})();

Pretty exciting stuff right? For now, our service does nothing but return an empty Object. Before we can go ahead and change that, we need to think about how we want our service to be used.

Lets assume we have a basic controller called TodoController that has a single method to list all of our current to-dos:

(function() {
	'use strict';

	angular.module('myAwesomeApp').controller('TodoController', TodoController);

	function TodoController($http) {
		vm.list = list;

		return vm;

		/**
		 * Returns a list of to-dos
		 */
		function list() {
			$http({
				method: 'GET',
				url: 'my/awesome/api/todo/list'
			}).then(
				function(response) {
					// We got our todos!
					vm.todos = response.data;
				},
				function(response) {
					// TODO: Oops! Something went wrong! Let's retry before informing the user!
				}
			);
		}
	}
})();

Angular’s $http service and Promises

As you can see, our list method uses the $http service to make a GET request to our /todo/list endpoint. If the request is successful, it sets the returned list of todos on the TodoController. If it fails, we want to retry fetching the todos again before informing the user that something went wrong. So, how do we do that?

Well, we could simply retry sending the request directly in the error callback function. This is definitely our quickest option, but isn’t viable unless we are only going to attempt retrying this one particular request. Otherwise we would have to duplicate this logic whenever we want to do the same thing elsewhere in our code. That’s not good. We should keep things as tight, clean and focused as possible. This is where our new HttpRetryService comes into play.

According to the Angular documentation, “The $http service is a function which takes a single argument — a configuration object — that is used to generate an HTTP request and returns a promise”. Instead of handling the Promise returned by the $http service in our controller’s list method, lets have our HttpRetryService do the work for us:

(function() {

    'use strict';

    angular
        .module('myAwesomeApp')
        .factory('HttpRetryService', HttpRetryService);

    function HttpRetryService() {

        var httpRetryService = {
            try: try
        };

        return httpRetryService;

        /**
         * Attempts to resolve a Promise, and retries if it fails
         * @param  promise {Promise}
         * @return {Promise}
         */
        function try(promise) {
            return promise
                .then(function(response) {
                    return response;
                }, function(response) {
                    // TODO: Oops! Something went wrong. Let's retry!
                });
        }
    }

})();

Okay, so what’s going on here? Lets break it down:

  1. For now, our service only needs one public method, try, which will accept a Promise as it’s only argument.
  2. It’s crucial to note that our try method resolves and returns the Promise (conveniently named promise). This will allow us to chain on the returned Promise from our Todo controller, and execute any controller-specific logic on the returned response.

Now all we have left to do is our retry logic. Before we jump into that, let’s update our TodoController to use our service in it’s current state.

(function() {
	'use strict';

	angular.module('myAwesomeApp').controller('TodoController', TodoController);

	function TodoController($http, $window, HttpRetryService) {
		vm.list = list;

		return vm;

		/**
		 * Returns a list of to-dos
		 */
		function list() {
			var promise = $http({
				method: 'GET',
				url: 'my/awesome/api/todo/list'
			});

			HttpRetryService.try(promise).then(
				function(response) {
					vm.todos = response.data;
				},
				function(response) {
					// We tried and re-tried, there's no hope. Let's inform the user.
					$window.alert('There was an error! ' + response.data.message);
				}
			);
		}
	}
})();

You’ll notice a few changes:

  1. We injected our new HttpRetryService.
  2. We stored the Promise returned by the $http service in it’s own variable, and passed it as an argument to the try method on our retry service.
  3. Since try simply tries resolving the promise and returns it, we can chain on the returned promise and trigger our controller-specific logic. In this case, we either set the list of todos on our controller, or alert the user that something went wrong.

Setting up our retry logic

The last and most important step is our retry logic in our service. As specified in the Angular docs, the response object returned by the $http service has a property called config, which is the configuration object that was used to generate the original request. We can use config to retry sending the same request an arbitrary number of times before giving up. Let’s modify our service to do just that.

(function() {

    'use strict';

    angular
        .module('myAwesomeApp')
        .factory('HttpRetryService', HttpRetryService);

    function HttpRetryService($http, $q) {

        var httpRetryService = {
            try: try
        };

        return httpRetryService;

        /**
         * Attempts to resolve a Promise, and retries if it fails
         *
         * @param promise {Promise}
         * @param numRetries (Int)
         * @return {Promise}
         */
        function try(promise, numRetries) {
            return promise
                .then(function(response) {
                    return response;
                }, function(response) {
                    var config = angular.extend({
                        retryCount: 0
                    }, response.config);

                    return _retry(config, numRetries || 3);
                });
        }

         /**
          * Retries sending a failed request
          *
          * @param config {Object}
          * @param numRetries {Int}
          * @return {Promise}
          */
        function _retry(config, numRetries) {
            return $http(config)
                .then(function(response) {
                    return response;
                }, function(response) {
                    config.retryCount++;
                    if (config.retryCount <= numRetries) {
                        return _retry(config, numRetries || 3);
                    } else {
                        return $q.reject(response);
                    }
                });
        }
    }

})();

We made quite a few modifications to our service, so let’s go through them step by step:

  1. We injected the $http and $q services.
  2. We added a numRetries argument to our try method which indicates the number of retries we should attempt before giving up on the request. In our error callback, we create a new config object by extending the config property on the returned response. We also keep track of the number of retries we have attempted by adding a property to our config object called retryCount and setting it to 0.
  3. We created a private retry function which takes a config object and numRetries as arguments. This is where all of our retry logic lives. retry is similar to try since it also returns a Promise (note that our try method returns _retry). We use the $http to make a request using config (the same configuration object from our original request). If the request is successful, we return the Promise. If the request fails, we increment the retryCount property set on config (from inside the try method), and then either attempt to retry the request or reject the Promise depending on the numRetries we are allowed.

Other then that, the logic in our TodoController does not change since we default the numRetries to 3 if it isn’t passed.

And there you have it - a clean, simple and easy to use retry service in Angular.