Duplicate POST requests on retry cause unintended data creation
0 reputation · 12 May 2021, 07:38 UTC
0 reputation · 12 May 2021, 07:38 UTC
The goal is to configure retry logic for create/update operations (POST/PUT/PATCH) that feed an Angular Material MatTable so that transient server errors are retried without causing duplicate records. The solution must keep the mat‑progress‑spinner visible during retries, avoid altering the original user‑initiated payload, and work with the existing CDK Directionality and Portal services.
Uncertainty remains about where to place the retryWhen operator relative to shareReplay or similar multicasting operators so that a failed attempt does not get cached and subsequent retries actually re‑send the HTTP call. Additionally, it is unclear how to generate and attach an idempotency key automatically within the data‑source stream without exposing the key to the UI or requiring manual handling in each component. Finally, developers wonder whether using a MatSnackBar action to trigger a retry can guarantee that the exact same request body is retried, or whether extra safeguards are needed to prevent duplicate writes. What is the recommended placement of retryWhen relative to shareReplay to ensure retries re‑execute the call? How can an idempotency key be added automatically to the request stream without UI exposure? Does a MatSnackBar‑triggered retry guarantee identical payloads, or must additional client‑side deduplication be applied?
32775 reputation · 12 May 2021, 16:55 UTC
To prevent duplicate record creation during retries, the retryWhen (or retry in RxJS 7+) operator must be placed before any multicasting operators like shareReplay. If shareReplay is placed before the retry logic, the observable will cache the failed response, and subsequent subscriptions will receive the cached error rather than triggering a new HTTP request.
The stream should follow this order to ensure the request is re-executed and the UI state remains consistent:
retryWhen handles transient errors and re-subscribes to the source.shareReplay(1) ensures multiple components (like the MatTable and a loading spinner) share the same execution result.To avoid exposing keys to the UI or manually adding them to every component, implement an HttpInterceptor. This allows the key to be generated and attached to the headers of every POST, PUT, or PATCH request automatically.
// Conceptual Interceptor Logic
intercept(req: HttpRequest<any>, next: HttpHandler) {
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
const idempotentReq = req.clone({
setHeaders: { 'X-Idempotency-Key': this.generateUuid() }
});
return next.handle(idempotentReq);
}
return next.handle(req);
}
A MatSnackBar action does not inherently guarantee an identical payload. If the retry is triggered by calling a function that re-reads values from a form or a state store, the payload may have changed. To guarantee the exact same request is retried, you must capture the original request object (including the idempotency key) in a closure or a service-level state variable and re-emit that specific object into the stream.
X-Idempotency-Key remains identical across all attempts for a single operation.200 OK or 201 Created for the first request and a 200 OK (returning the existing resource) or 409 Conflict for subsequent requests with the same key.Diagnostic Requirement: Please specify if your backend is using a database with unique constraints on natural keys or a dedicated idempotency table, as this determines whether the client should expect a 200 or a 409 response on successful deduplication.
Use comments to ask for clarification. Post a solution as an answer.
32,775 reputation · 12 May 2021, 16:27 UTC
When using shareReplay for loading state, add resetOnError: true (RxJS 7+) so that a failed HTTP response is not cached; otherwise the error would be replayed on subsequent subscriptions and retryWhen would never re‑execute the call. Place the idempotency‑key injection (via map or an interceptor) before retryWhen, then shareReplay(1, { refCount: true, resetOnError: true }) after the retry logic. This order guarantees each retry issues a fresh POST with the same key, while the MatTable and spinner stay subscribed to the latest execution. A MatSnackBar action that simply re‑invokes the same request‑factory function will resend the identical payload, provided the factory does not depend on mutable component state; otherwise extract the payload into a pure constant or service method to guarantee consistency.