Idempotency & Retry-Safe Systems · Idempotency Keys in Practice
Request Fingerprints and Key Misuse
Detect reuse of a key with a different request.
A client bug might reuse a key for a different operation. Store a fingerprint — a hash of method, path and a canonicalized body (sorted keys, normalized numbers) — and reject mismatches with 422 instead of replaying an unrelated response. Exclude volatile fields (timestamps, tracing headers) from the fingerprint, and document which parts are included.
Fingerprint contents
| Include in fingerprint | Exclude |
|---|---|
| HTTP method and path | Tracing headers (traceparent) |
| Canonical request body | Client timestamps meant to vary |
| Query parameters that change behavior | Auth token (scope covers identity) |
import hashlib, json
def fingerprint(method: str, path: str, body: dict) -> str:
canonical = json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
return hashlib.sha256(f"{method}\n{path}\n{canonical}".encode()).hexdigest()
import { createHash } from "node:crypto";
const canonical = (v: unknown): unknown =>
Array.isArray(v) ? v.map(canonical)
: v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, canonical((v as any)[k])]))
: v;
export const fingerprint = (method: string, path: string, body: unknown) =>
createHash("sha256").update(`${method}\n${path}\n${JSON.stringify(canonical(body))}`).digest("hex");
static String fingerprint(String method, String path, JsonNode body, ObjectMapper mapper) throws Exception {
ObjectMapper sorted = mapper.copy().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
Object canonical = sorted.treeToValue(body, Object.class); // maps get sorted on write
String payload = method + "\n" + path + "\n" + sorted.writeValueAsString(canonical);
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload.getBytes(UTF_8)));
}
Common pitfalls
- Hashing raw bytes (whitespace/key order differences create false mismatches).
- Silently replaying responses for different requests.
- Including volatile fields so legitimate retries fail.
Key points
- Canonicalize then hash
- Mismatch → 422
- Document included fields
Further reading: IETF draft: Idempotency-Key header
Practice questions
Why canonicalize JSON before hashing?
Answer
Semantically identical bodies can differ in key order or whitespace; canonicalization ensures retries produce the same fingerprint.
What status code signals reuse of an idempotency key with a different payload?
Answer
422 Unprocessable Content, per the IETF Idempotency-Key draft.
Open in the app to mark lessons complete, take notes and track your progress.