AngularJS $http interceptor retry: scoping automatic retries to idempotent requests only
0 reputation · 20 Aug 2023, 05:13 UTC
0 reputation · 20 Aug 2023, 05:13 UTC
I'm designing a responseError interceptor (registered via $httpProvider.interceptors) that re-issues failed requests through $http with a bounded retry count and $timeout-based backoff. The goal is transparent retry for transient failures (5xx, timeouts) without ever duplicating a server-side write.
The unresolved part is the safety boundary. GET/PUT/DELETE are idempotent by HTTP semantics, so retrying them is safe, but a POST that times out may have already been processed server-side, and a blind retry would create a duplicate record. I also need the interceptor to skip requests that should fail fast, and to return the retried promise so the original caller sees the final result rather than an early rejection.
Assume AngularJS 1.6.x. Server-side idempotency-key deduplication is not yet implemented, so the client design must not depend on it.
1. What is a clean way to mark requests as retry-safe (e.g., an opt-in flag on the config object) so the interceptor only retries idempotent or explicitly flagged operations?
2. Should retry eligibility be decided by HTTP method alone, or is there a documented pattern for scoping it per-request in the interceptor?
3. How should the retry counter live on the config object so concurrent requests don't interfere with each other's attempt limits?
31775 reputation · 20 Aug 2023, 14:48 UTC
In AngularJS 1.6.x you can scope automatic retries to idempotent or explicitly flagged requests by:
config.retrySafe) together with a method whitelist.$timeout-wrapped $http call from the responseError interceptor; the returned promise chains to the original caller’s promise.angular.module('app')
.factory('retryInterceptor', ['$q', '$timeout', '$http', function ($q, $timeout, $http) {
var MAX_ATTEMPTS = 3;
var BASE_BACKOFF = 300; // ms
function isRetryable(rejection, config) {
// 1. method whitelist for inherent idempotency
var safeMethods = /^(GET|HEAD|PUT|DELETE|OPTIONS)$/i;
var methodOk = safeMethods.test(config.method);
// 2. opt‑in flag for non‑idempotent calls that are known safe
var flagOk = config.retrySafe === true;
// 3. transient error condition
var status = rejection.status;
var transient = status === 0 || status === 408 || (status >= 500 && status < 600);
// 4. attempt counter stored on config
var attempt = (config.__retryCount || 0) + 1;
return (methodOk || flagOk) && transient && attempt <= MAX_ATTEMPTS;
}
return {
responseError: function (rejection) {
var config = rejection.config;
if (!isRetryable(rejection, config)) {
return $q.reject(rejection); // fail fast
}
// increment counter on the config before re‑issuing
config.__retryCount = (config.__retryCount || 0) + 1;
var backoff = BASE_BACKOFF * Math.pow(2, config.__retryCount - 1); // exponential
// return a promise that resolves after backoff with a new $http call
return $timeout(function () {
return $http(config);
}, backoff);
}
};
}])
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('retryInterceptor');
}]);
config.retrySafe = true.Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.