15

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?

S.Klechkovski
  • 4,005
  • 16
  • 27
Dan
  • 55,715
  • 40
  • 116
  • 154
  • Lost internet connection can be handled too (HTTP 0 status). Do you want me to add it in the answer? – S.Klechkovski Sep 19 '15 at 11:18
  • can you tell us how can we help you solving the question if you are not satisfied with the answers? If it's already solved with the help from the answers it would be good to handle the bounty appropriately or if you solved it by yourself it would be good to provide your solution. Thanks. – S.Klechkovski Sep 26 '15 at 07:37

3 Answers3

21

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.

jangosteve
  • 1,562
  • 2
  • 14
  • 26
S.Klechkovski
  • 4,005
  • 16
  • 27
  • 1
    Can you provide more info about the reason why it's failing? – S.Klechkovski Sep 23 '15 at 06:18
  • the reason of request fails is server overload. The server returns 504 response if the queue is too big or it's running out of memory – Dan Sep 29 '15 at 12:19
  • IMHO you should address this issue on the server side. If you can't do that because of some reasons the best you can do it to add timeout before retries and hope that on the next request the server will be available. – S.Klechkovski Sep 29 '15 at 12:26
  • However, I can't inject $timeout into config because angular has not finished bootstrapping. What can I do? – Dan Sep 29 '15 at 23:46
  • 1
    That would be the same as we do for the $http service. var $timeout = $injector.get('$timeout'); – S.Klechkovski Sep 30 '15 at 09:00
  • FYI I updated the code snippet in the answer, as the code example given wouldn't actually increment the timeout or ever stop retrying (since the `incrementalTimeout` wouldn't ever increase and would always be < 5000). This is because the `retryRequest` function returned before the `incrementalTimeout *=2` ever ran. – jangosteve Feb 10 '17 at 13:54
  • 1
    Very useful ! In my case I'm using `response.status === -1` to check for canceled / timed out request (see [this post](https://github.com/angular/angular.js/issues/12920#issuecomment-142532762) ) – Daniel Mar 07 '17 at 11:47
  • Is there any situation where i can test this code. Suggest me because i am working in local environment where server responds too fast. I want to know the real time scenario where i can test this code. Thanks. It would be great help if you could look this question https://stackoverflow.com/questions/44159088/abort-cancel-abandon-a-http-request-in-angular-js-and-retry-it-again/44159498 @S.Klechkovski – Redhya May 25 '17 at 04:49
3

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);
        }
    };
});
Cameron
  • 1,524
  • 11
  • 21
  • 2
    I'm an Angular newbie, but it seems to me that $timeout should be used here instead of setTimeout, for purpose of piping the state of the newly created $http promise into failed `$http` promise. In other words, the promise that created 500 request continues to live, and should fire `success` callback if retry succeeds. – Dan Sep 30 '15 at 10:02
  • @Dan You're right... `setTimeout` swallows a response error, and the original request's success function gets called with undefined data after all retries failed. Piping the state propagates `success` or `error`. – builder Mar 28 '16 at 02:53
  • 2
    Also, if I'm not mistaken, retries is shared for every request, so if multiple request would fail at the same time, this solution will not work. – Thomas Stubbe Oct 07 '16 at 15:05
  • you should use var retryCounter = [] and add the url as key + count as value. Unset it afterwards. – Thomas Stubbe Oct 07 '16 at 15:08
0

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

Tushar Walzade
  • 3,737
  • 4
  • 33
  • 56