Non-idempotent request duplication during $http retry logic
0 reputation · 25 Jun 2020, 14:23 UTC
0 reputation · 25 Jun 2020, 14:23 UTC
AngularJS 1.x does not provide a native mechanism for automatic request retries or built-in deduplication for HTTP calls. When implementing custom retry logic via interceptors or wrapper services, there is a risk of duplicating writes for non-idempotent methods such as POST, PUT, or PATCH if a request fails due to a network timeout after the server has already processed the operation.
Because the $http service does not track request identity or provide a framework-level idempotency key, the responsibility for preventing duplicate data entries falls entirely on the application layer.
Should a standardized retry helper default to retrying only safe methods, and if so, what is the recommended pattern for opting into unsafe method retries without disrupting existing interceptor chains?
34775 reputation · 25 Jun 2020, 19:45 UTC
Yes. A standardized retry helper should default to retrying only safe methods (GET, HEAD) and idempotent methods (PUT, DELETE) when the server contract guarantees idempotency. Unsafe methods (POST, PATCH) must require explicit opt-in per request.
$http has no built-in retry logic, request IDs, or attempt tracking.Add a single, centralized response interceptor that owns the retry policy. It inspects config.method and an explicit opt-in flag before re-issuing.
app.factory('retryInterceptor', ['$q', '$http', function($q, $http) {
var SAFE_METHODS = ['GET', 'HEAD'];
var IDEMPOTENT_METHODS = ['PUT', 'DELETE'];
var RETRYABLE_STATUS = [0, 408, 429, 500, 502, 503, 504];
return {
responseError: function(rejection) {
var config = rejection.config;
if (!config || config.__retryAttempt >= 3) {
return $q.reject(rejection);
}
var method = (config.method || 'GET').toUpperCase();
var isSafe = SAFE_METHODS.indexOf(method) !== -1;
var isIdempotent = IDEMPOTENT_METHODS.indexOf(method) !== -1;
var callerOptIn = config.retryUnsafe === true;
if (!(isSafe || isIdempotent || callerOptIn)) {
return $q.reject(rejection);
}
if (RETRYABLE_STATUS.indexOf(rejection.status) === -1) {
return $q.reject(rejection);
}
config.__retryAttempt = (config.__retryAttempt || 0) + 1;
return $http(config);
}
};
}]);
For POST/PATCH endpoints that accept an idempotency key (generated by the client or returned by a prior GET), set the flag on the specific request:
$http.post('/api/orders', payload, {
headers: { 'Idempotency-Key': generateKey() },
retryUnsafe: true
});
This keeps the interceptor chain intact—no extra interceptors, no config mutation outside the retry decision point.
The client cannot guarantee exactly-once semantics for non-idempotent calls. The API must:
Idempotency-Key header and deduplicate within a TTL window.409 Conflict or the original response on duplicate keys.Does your API already implement idempotency-key handling on the unsafe endpoints you intend to retry, or will that need to be added first?
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 25 Jun 2020, 21:37 UTC
While the suggested opt-in flag allows the client to trigger a retry for unsafe methods, it does not solve the underlying risk of server-side duplication if the initial request was actually processed before the timeout occurred.
To make POST or PATCH retries truly safe, the application should implement a client-generated Idempotency Key. This involves adding a unique identifier (e.g., a UUID) to a custom header, such as Idempotency-Key, in the original request config. When the interceptor re-issues the request, it must reuse the exact same key from the original config object.
$http, ensuring it is attached to the headers object.