Skip to content

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 { idempotency } from "@centralping/ergo";
OptionTypeDefaultDescription
storeobjectIn-memory storePluggable store with get/set/complete/delete
ttlMsnumber86400000 (24h)TTL for default in-memory store
requiredbooleanfalseReturn 400 if header is missing on applicable methods
methodsSet<string> | string[]POST, PATCHHTTP methods to enforce

Depends on the request state:

StateReturn
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.

The Idempotency-Key header value must be an RFC 8941 structured field string — a double-quoted value on the wire:

Terminal window
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.

StatusCondition
400 Bad RequestHeader is present but not a valid RFC 8941 sf-string (always, regardless of required)
400 Bad RequestHeader is absent and required: true
409 ConflictSame key with a different request fingerprint
409 ConflictConcurrent 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 } },
}),
);

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;
}
},
);
  1. New request — the middleware returns { key, fingerprint, complete, discard } on acc.idempotency. The execute handler runs normally.
  2. complete(response) — stores the response for this key. The argument should be a response accumulator shape (e.g. { statusCode, body }).
  3. 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.
  4. discard() — removes the key entry, allowing the next request with that key to be treated as new.

See the auto-generated idempotency API docs.