Timeout or 5xx failure on AngularJS $http POST/PUT/PATCH can duplicate writes on retry
0 reputation · 19 Nov 2022, 14:24 UTC
0 reputation · 19 Nov 2022, 14:24 UTC
AngularJS $http provides no built-in retry mechanism, so applications that retry failed POST, PUT, or PATCH requests must implement custom logic. Because these methods are not inherently idempotent, a client-side timeout or 5xx response does not guarantee the server aborted the original write. A subsequent retry can therefore create duplicate records or apply the same mutation twice.
The framework reached end of life, so no official updates to $http will add retry counts, idempotency-key headers, or automatic deduplication. Any solution must be built with interceptors, $q chains, or wrapper services that filter by HTTP method and URL. Server-side support for deduplication tokens cannot be assumed.
34775 reputation · 19 Nov 2022, 18:56 UTC
You cannot make client-side retries of non-idempotent AngularJS $http requests fully safe without server-side deduplication. The strongest available pattern is to retry only when the original request definitively never reached the server (a transport-level timeout with no response), attach a stable idempotency key to every attempt through an interceptor, and cancel the in-flight original before issuing the retry. This reduces duplicate-write risk but does not eliminate it.
AngularJS $http has no built-in retry, idempotency-key, or deduplication support, and the framework is end of life. A 5xx response or client timeout means the outcome is unknown, not that the write failed. If the server committed the mutation before the connection dropped, any retry duplicates it. Client-side cancellation and idempotency headers help only if the server honors them.
In an $http response interceptor, retry only when the rejection carries no response object—typically a timeout or network error where rejection.status === 0. Do not retry on 5xx responses unless you can prove the server did not process the write; a 500 after a successful database commit is indistinguishable from a pre-processing failure to the client.
angular.module('app').factory('retryInterceptor', function($q, $injector) {
return {
responseError: function(rejection) {
// Only retry transport failures with no server response
if (rejection.status === 0 && rejection.config && /^(POST|PUT|PATCH)$/.test(rejection.config.method)) {
var $http = $injector.get('$http');
var config = rejection.config;
// Reuse the same idempotency key if already set
var key = config.headers['X-Idempotency-Key'] || generateKey();
config.headers['X-Idempotency-Key'] = key;
return $http(config);
}
return $q.reject(rejection);
}
};
});Replace generateKey() with a UUID generator. This wrapper is intentionally narrow: it retries only unsafe methods on status 0.
Use a request interceptor to inject X-Idempotency-Key for unsafe methods. Store the key on the request config so retries reuse the same value. Because the interceptor runs before every attempt, the retry inherits the header automatically.
angular.module('app').factory('idempotencyInterceptor', function() {
return {
request: function(config) {
if (/^(POST|PUT|PATCH)$/.test(config.method) && !config.headers['X-Idempotency-Key']) {
config.headers['X-Idempotency-Key'] = generateKey();
}
return config;
}
};
});Assumption: the server validates and deduplicates this header. If it does not, the key is inert and duplicates remain possible.
AngularJS supports a timeout property on the request config that accepts a promise. Resolving that promise aborts the underlying XHR. To avoid racing the original write, create a fresh canceler for each attempt, resolve the old one before sending the retry, and verify in browser devtools that the original request shows as canceled.
function buildConfig(method, url, data) {
var canceler = $q.defer();
return {
method: method,
url: url,
data: data,
timeout: canceler.promise,
_canceler: canceler // retained for manual abort
};
}Caution: canceling an in-flight write on a flaky network may abort a request the server actually received and processed. Cancellation reduces the race window but does not close it.
X-Idempotency-Key values and rejects duplicates; confirm the same key appears on retries.If your API does not implement idempotency-key deduplication, no client-side interceptor can guarantee exactly-once semantics. In that case, the only safe client strategy is to avoid automatic retries for unsafe methods and surface the failure to the user for manual reconciliation. If you can share whether your server already honors idempotency keys, that detail would determine whether the interceptor approach is sufficient or whether retries should be disabled entirely.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.