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?
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.
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);
}
};
});
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
Related
Below is an example of a pretty crude way i'm currently handling a a HTTP call which is taking too long.
If it takes more than 3 seconds, re-fire, and if this fails 4 times than exit.
While it works, I don't wish to create every HTTP request like this as it is too much code for one call.
Is there a way of applying this globally?
I'm not sure if an interceptor is suitable for this case, as I need the error handle to kick in after the HTTP call takes too long, rather than when it receives a bad response from the server.
I would also need this to apply to requests for partial views.
Code:
var errorCount = 0;
function someFunction() {
var startTime = new Date().getTime();
var deferred = $q.defer();
var url = 'some url';
$http.get(url, {cache: true, timeout: 3000}).success(function(response) {
deferred.resolve(response);
}).error(function(result, status, header, config) {
var respTime = new Date().getTime() - startTime;
if( respTime >= config.timeout ){
errorCount +=1;
if ( errorCount > 3) {
errorCount = 0;
deferred.reject(result);
} else {
getCountries();
};
} else{
deferred.reject(error);
};
});
return deferred.promise;
};
Ok so this is what I ended up doing.
Probably not the best way but maybe someone will find it useful too!
So using an interceptor I configure all outgoing HTTP requests to abort after 3 seconds, and configure any responses which have a status of 'abort' (which is a status of '-1' I think) to retry 3 times.
Code:
(function() {
'use strict';
angular
.module('InterceptModule', []);
angular
.module('InterceptModule')
.factory('myInterceptor', myInterceptor);
myInterceptor.$inject = ['$q', '$injector','$cookies']
function myInterceptor($q,$injector,$cookies) {
var errorCount = 0;
var service = {
responseError: responseError,
request : request
};
return service;
//////////
/* this function is where tell the request to re-fire if it
* has been aborted.
*
*/
function responseError(response) {
if ( response.status == -1 ) {
var deferred = $q.defer();
var firstResponse = response;
var url = response.config.url;
var $http = $injector.get('$http');
$http.get(url, {cache: true}).then(function(response) {
deferred.resolve(response);
}, function(result, status, header, config) {
if ( status == -1 ) {
errorCount += 1;
if ( errorCount > 3 ) {
errorCount = 0;
deferred.reject(result);
} else {
responseError(firstResponse);
}
};
deferred.reject(result);
});
return deferred.promise;
};/* if */
return $q.reject(response);
};
/**
* this function is where we configure our outgoing http requests
* to abort if they take longer than 3 seconds.
*
*/
function request(config) {
config.timeout = 3000;
return config;
};
}/* service */
})();
Inject our intercept module
angular
.module('myApp', ['InterceptModule']);
Then push our interceptor in our config
$httpProvider.interceptors.push('myInterceptor');
In my application, I have a service called 'pendingRequests' that keeps track of my http requests. I configured my $httpProvider to use this service.
The purpose of this service is to give me the ability to cancel ALL pending http requests occurring in ANY controller.
Here is the code:
app.service('pendingRequests', function($rootScope, $q) {
var pending = [];
this.get = function() {
return pending;
};
this.add = function(request) {
pending.push(request);
//console.log("Pending Requests(before):" + pending);
};
this.remove = function(request) {
angular.forEach(pending, function(p , key) {
if(p.url == request.url) pending.splice(key, 1);
});
// console.log("Pending Requests(after):" + pending);
};
this.cancelAll = function() {
if(typeof pending !='undefined'){
angular.forEach(pending, function(p) {
p.canceller.resolve();
});
pending.length = 0;
}
};
});
app.config(function($httpProvider){
$httpProvider.interceptors.push(function($q, pendingRequests){
return {
'request': function (config){
var canceller = $q.defer();
pendingRequests.add({
url: config.url,
canceller: canceller
});
config.timeout = canceller.promise;
return config || $q.when(config);
},
'response': function (response){
pendingRequests.remove(response.config);
//pendingRequests remove request
return response;
}
}
});
});
The service is canceling the requests as intended. However, the next request submitted is delayed as if it is still waiting for another request to complete.
What is causing this delay?
How can I stop a request in Angularjs interceptor.
Is there any way to do that?
I tried using promises and sending reject instead of resolve !
.factory('connectionInterceptor', ['$q', '$timeout',
function($q, $timeout) {
var connectionInterceptor = {
request: function(config) {
var q = $q.defer();
$timeout(function() {
q.reject();
}, 2000)
return q.promise;
// return config;
}
}
return connectionInterceptor;
}
])
.config(function($httpProvider) {
$httpProvider.interceptors.push('connectionInterceptor');
});
I ended up bypassing angular XHR call with the following angular Interceptor:
function HttpSessionExpiredInterceptor(sessionService) {
return {
request: function(config) {
if (sessionService.hasExpired()) {
/* Avoid any other XHR call. Trick angular into thinking it's a GET request.
* This way the caching mechanism can kick in and bypass the XHR call.
* We return an empty response because, at this point, we do not care about the
* behaviour of the app. */
if (_.startsWith(config.url, '/your-app-base-path/')) {
config.method = 'GET';
config.cache = {
get: function() {
return null;
}
};
}
}
return config;
}
};
}
This way, any request, POST, PUT, ... is transformed as a GET so that the caching mechanism can be
used by angular. At this point, you can use your own caching mechanism, in my case, when session
expires, I do not care anymore about what to return.
The $http service has an options
timeout to do the job.
you can do like:
angular.module('myApp')
.factory('httpInterceptor', ['$q', '$location',function ($q, $location) {
var canceller = $q.defer();
return {
'request': function(config) {
// promise that should abort the request when resolved.
config.timeout = canceller.promise;
return config;
},
'response': function(response) {
return response;
},
'responseError': function(rejection) {
if (rejection.status === 401) {
canceller.resolve('Unauthorized');
$location.url('/user/signin');
}
if (rejection.status === 403) {
canceller.resolve('Forbidden');
$location.url('/');
}
return $q.reject(rejection);
}
};
}
])
//Http Intercpetor to check auth failures for xhr requests
.config(['$httpProvider',function($httpProvider) {
$httpProvider.interceptors.push('httpInterceptor');
}]);
Not sure if it is possible in general. But you can start a $http request with a "canceler".
Here is an example from this answer:
var canceler = $q.defer();
$http.get('/someUrl', {timeout: canceler.promise}).success(successCallback);
// later...
canceler.resolve(); // Aborts the $http request if it isn't finished.
So if you have control over the way that you start your request, this might be an option.
I just ended up in returning as an empty object
'request': function request(config) {
if(shouldCancelThisRequest){
return {};
}
return config;
}
Here is what works for me, especially for the purposes of stopping the outgoing request, and mocking the data:
app
.factory("connectionInterceptor", [
"$q",
function ($q) {
return {
request: function (config) {
// you can intercept a url here with (config.url == 'https://etc...') or regex or use other conditions
if ("conditions met") {
config.method = "GET";
// this is simulating a cache object, or alternatively, you can use a real cache object and pre-register key-value pairs,
// you can then remove the if block above and rely on the cache (but your cache key has to be the exact url string with parameters)
config.cache = {
get: function (key) {
// because of how angularjs $http works, especially older versions, you need a wrapping array to get the data
// back properly to your methods (if your result data happens to be an array). Otherwise, if the result data is an object
// you can pass back that object here without any return codes, status, or headers.
return [200, mockDataResults, {}, "OK"];
},
};
}
return config;
},
};
},
])
.config(function ($httpProvider) {
$httpProvider.interceptors.push("connectionInterceptor");
});
If you are trying to mock a result like
[42, 122, 466]
you need to send an array with some http params back, its just how the ng sendReq() function is written unfortunately. (see line 1414 of https://github.com/angular/angular.js/blob/e41f018959934bfbf982ba996cd654b1fce88d43/src/ng/http.js#L1414 or snippet below)
// from AngularJS http.js
// serving from cache
if (isArray(cachedResp)) {
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3], cachedResp[4]);
} else {
resolvePromise(cachedResp, 200, {}, 'OK', 'complete');
}
I've got this interceptor setup to read XML I receive on all my requests:
https://gist.github.com/SantechDev/539a70208d23d8918ce0
Now when the server returns a 500 error, it doesn't seem like the response goes through the interceptor. I tried logging response but nothing comes up
Would anyone know why?
I don't know how yours has to work but the ones I wrote look totally different..
var interceptor = ['$rootScope', '$q', "Base64", function (scope, $q, Base64) {
function success(response) {
return response;
}
function error(response) {
var status = response.status;
if (status == 401) {
window.location = "/account/login?redirectUrl=" + Base64.encode(document.URL);
return;
}
// otherwise
return $q.reject(response);
}
return function (promise) {
return promise.then(success, error);
}
}];
$httpProvider.responseInterceptors.push(interceptor);
you can check out the full code here.
I'm building my first Angular app, and I've run into a problem. I have an AngularJS resource whose .get I call with both a success and error callback, like so:
var new_uptime = Uptime.get({}, function(){
console.log("new_uptime", new_uptime, "$scope.uptime", $scope.uptime);
console.log("new_uptime.uptime", new_uptime.uptime);
if ($scope.uptime && new_uptime.uptime < $scope.uptime.uptime) {
console.log("Reboot detected");
location.reload(true);
}
$scope.uptime = new_uptime;
$timeout(reload_uptime, 5000);
}, function(response){
console.log("Failed to read uptime", response);
console.log("Retrying in 10s");
$scope.uptime = "[unknown]";
$timeout(reload_uptime, 10000);
});
I've tested this with various error statuses from the server, and it works fine. Now I'd like to add app-wide retrying on 503s. I've added a module for that, which looks like this:
retry_module = angular.module('service.retry', []);
retry_module.factory("retry", ["$q", "$injector", "$timeout", function($q, $injector, $timeout) {
return {
"responseError": function(rejection) {
console.log("Request failed", rejection);
if (rejection.status != 503) {
console.log("Unhandled status");
return $q.reject(rejection);
}
var delay = Math.floor(Math.random() * 1000);
console.log("Was 503, retrying in " + delay + "ms");
var deferred = $q.defer();
$timeout(function() {
var $http = $http || $injector.get("$http");
deferred.resolve($http(rejection.config));
}, delay);
return deferred;
}
};
}]);
retry_module.config(["$httpProvider", function ($httpProvider) {
$httpProvider.interceptors.push("retry");
}]);
This works well for retrying on 503s, and successful attempts are handled by the success callback in the Uptime.get caller, as intended. However, if the server returns some other error, then I get the "Unhandled status" printout, but then neither "new_uptime" nor "Failed to read uptime". Where did my rejection go, and how do I go about trouble-shooting issues like this?
You need to return the promise, not the deferred. So the end of your responseError handler should be:
var deferred = $q.defer();
$timeout(function() {
var $http = $http || $injector.get("$http");
deferred.resolve($http(rejection.config));
}, delay);
return deferred.promise;