Retry failed requests with $http interceptor

All we need is an easy explanation of the problem, so here it is.

The API my webapp is talking to sometimes overloads and is sending 500 Internal Server Error if it cannot handle request.

There are 100+ different requests my web application can send, so if I implement retry on each individually, it will cost me hours of typing.

I’m already using $httpProvider interceptor, here it is (simplified)

$httpProvider.interceptors.push(function ($q) {
    return {
        responseError: function (response) {
            switch (response.status) {
                case 401 :
                    window.location = "/";
                    alert('Session has expired. Redirecting to login page');
                    break;
                case 500 :
                    // TODO: retry the request
                    break;
            }
            return $q.reject(response);
        }
    };
});

How could I resend a request after getting 500 response code from the server?

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

Angular provides reference to the config object which was used by $http service for doing the request in the response (response.config). That means if we can inject $http service in the interceptor we can easily resend the request. Simple injecting of $http service in the interceptor is not possible because of the circular dependency but luckily there is a workaround for that.

This is an example how the implementation of a such interceptor can be done.

$httpProvider.interceptors.push(function ($q, $injector) {
    var incrementalTimeout = 1000;

    function retryRequest (httpConfig) {
        var $timeout = $injector.get('$timeout');
        var thisTimeout = incrementalTimeout;
        incrementalTimeout *= 2;
        return $timeout(function() {
            var $http = $injector.get('$http');
            return $http(httpConfig);
        }, thisTimeout);
    };

    return {
        responseError: function (response) {
            if (response.status === 500) {
                if (incrementalTimeout < 5000) {
                    return retryRequest(response.config);
                }
                else {
                    alert('The remote server seems to be busy at the moment. Please try again in 5 minutes');
                }
            }
            else {
                incrementalTimeout = 1000;
            }
            return $q.reject(response);
        }
    };
});

Note: In this example implementation the interceptor will retry the request until you receive a response with status that is different than 500. Improvement to this can be adding some timeout before retrying and retrying only once.

Method 2

You can check for any possible server side errors by expanding the status code check a little more. This interceptor will attempt to retry the request multiple times and will do so on any response code 500 or higher. It will wait 1 second before retrying, and give up after 3 tries.

$httpProvider.interceptors.push(function ($q, $injector) {

    var retries = 0,
        waitBetweenErrors = 1000,
        maxRetries = 3;

    function onResponseError(httpConfig) {
        var $http = $injector.get('$http');
        setTimeout(function () {
            return $http(httpConfig);
        }, waitBetweenErrors);
    }

    return {
        responseError: function (response) {
            if (response.status >= 500 && retries < maxRetries) {
                retries++;
                return onResponseError(response.config);
            }
            retries = 0;
            return $q.reject(response);
        }
    };
});

Method 3

I wanted to retry requests in my response block also, so combining multiple answers from different posts from SO, I’ve written my interceptor as follows –

app.config(['$httpProvider', function ($httpProvider) {
    $httpProvider.interceptors.push(['$rootScope', '$cookies', '$q', '$injector', function ($rootScope, $cookies, $q, $injector) {
        var retries = 0, maxRetries = 3;

        return {
            request: function (config) {
                var csrf = $cookies.get("CSRF-Token");
                config.headers['X-CSRF-Token'] = csrf;
                if (config.data) config.data['CSRF-Token'] = csrf;
                return config;
            },
            response: function (r) {
                if (r.data.rCode == "000") {
                    $rootScope.serviceError = true;
                    if (retries < maxRetries) {
                        retries++;
                        var $http = $injector.get('$http');
                        return $http(r.config);
                    } else {
                        console.log('The remote server seems to be busy at the moment. Please try again in 5 minutes');
                    }
                }
                return r;
            },
            responseError: function (r) {
                if (r.status === 500) {
                    if (retries < maxRetries) {
                        retries++;
                        var $http = $injector.get('$http');
                        return $http(r.config);
                    } else {
                        console.log('The remote server seems to be busy at the moment. Please try again in 5 minutes');
                    }
                }

                retries = 0;
                return $q.reject(r);
            }
        }
    }]);
}])

credits to @S.Klechkovski, @Cameron & https://stackoverflow.com/a/20915196/5729813

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply