idempotency
Implements the Idempotency-Key HTTP header draft specification. Replays stored responses for duplicate request keys and detects fingerprint conflicts for concurrent or mismatched requests.
Place after body() in the pipeline (Stage 3) so the request fingerprint
can include the parsed body.
Pipeline stage: Validation (after body parsing)
Import
Section titled “Import”import { idempotency } from "@centralping/ergo";Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
store | object | In-memory store | Pluggable store with get/set/complete/delete |
ttlMs | number | 86400000 (24h) | TTL for default in-memory store |
required | boolean | false | Return 400 if header is missing on applicable methods |
methods | Set<string> | string[] | POST, PATCH | HTTP methods to enforce |
Return Value
Section titled “Return Value”Depends on the request state:
| State | Return |
|---|---|
| Method not applicable or key absent (not required) | {} |
| New request | { value: { key, fingerprint, complete(response), discard() } } |
| Replay (stored response exists) | { value: { replayed: true }, response: storedResponse } |
For new requests, call complete(response) after your execute stage to
store the response for future replays, or discard() to remove the key.
Header Format
Section titled “Header Format”The Idempotency-Key header value must be an
RFC 8941 structured field string
— a double-quoted value on the wire:
curl -X POST https://api.example.com/payments \ -H 'Idempotency-Key: "my-unique-key-123"' \ -H 'Content-Type: application/json' \ -d '{"amount": 1000}'Unquoted values (e.g., Idempotency-Key: my-key) are rejected with a
400 response that includes format guidance.
Error Responses
Section titled “Error Responses”| Status | Condition |
|---|---|
| 400 Bad Request | Header is present but not a valid RFC 8941 sf-string (always, regardless of required) |
| 400 Bad Request | Header is absent and required: true |
| 409 Conflict | Same key with a different request fingerprint |
| 409 Conflict | Concurrent request with the same key still processing |
import { compose, body, idempotency } from "@centralping/ergo";
const pipeline = compose( {fn: body(), setPath: "body"}, {fn: idempotency({ required: true }), setPath: "idempotency"}, (req, res, acc) => ({ response: { statusCode: 201, body: { created: true } }, }),);router.post("/payments", { idempotency: { required: true }, validate: { body: paymentSchema, }, execute: async (req, res, acc) => { const result = await processPayment(acc.body.parsed); return { response: { statusCode: 201, body: result } }; },});Storing Responses for Replay
Section titled “Storing Responses for Replay”For new requests, the middleware returns complete() and discard()
callbacks. Call complete(response) after your execute logic to store
the response for future replays, or discard() to remove the
idempotency entry (e.g. on failure).
import { compose, body, idempotency } from "@centralping/ergo";
const pipeline = compose( {fn: body(), setPath: "body"}, {fn: idempotency({ required: true }), setPath: "idempotency"}, async (req, res, acc) => { try { const result = await processPayment(acc.body.parsed); const response = { statusCode: 201, body: result };
acc.idempotency.complete(response);
return { response }; } catch (err) { acc.idempotency.discard(); throw err; } },);router.post("/payments", { idempotency: { required: true }, validate: { body: paymentSchema, }, execute: async (req, res, acc) => { try { const result = await processPayment(acc.body.parsed); const response = { statusCode: 201, body: result };
acc.idempotency.complete(response);
return { response }; } catch (err) { acc.idempotency.discard(); throw err; } },});Lifecycle
Section titled “Lifecycle”- New request — the middleware returns
{ key, fingerprint, complete, discard }onacc.idempotency. The execute handler runs normally. complete(response)— stores the response for this key. The argument should be a response accumulator shape (e.g.{ statusCode, body }).- Replay — a subsequent request with the same key and fingerprint returns the stored response immediately. The execute handler does not run — the middleware handles replay automatically.
discard()— removes the key entry, allowing the next request with that key to be treated as new.
RFC References
Section titled “RFC References”API Reference
Section titled “API Reference”See the auto-generated idempotency API docs.