Fixes a silent bug where idempotencyMiddleware returned a cached response even when the new request body differed from the original. The middleware now computes a canonical SHA-256 payload fingerprint and returns 409 Conflict with error code IDEMPOTENCY_KEY_REUSE_MISMATCH when fingerprints differ, along with a conflictingSummary that helps clients diagnose the mismatch without exposing stored sensitive values.
src/middleware/idempotency.ts- Exported
IDEMPOTENCY_KEY_REUSE_MISMATCHconstant (replaces inline'IDEMPOTENCY_CONFLICT'string) - Mismatch 409 response now includes
conflictingSummarywithidempotencyKey,incomingPayloadFingerprint,storedPayloadFingerprint, andincomingFields(sorted top-level key names only — no values leaked) - Structured logger warning on mismatch with both hashes for ops tracing
- Exported
src/middleware/idempotency.test.ts- Full rewrite with shared
makeDb/makeReq/makeReshelpers - 6 canonicalization tests for
calculateRequestHash - 6 mismatch-specific tests (issue #427 acceptance criteria)
- 3 in-progress/error-path tests
- Full rewrite with shared
| Criterion | Covered by |
|---|---|
| Mismatch returns 409 | returns 409 with IDEMPOTENCY_KEY_REUSE_MISMATCH when payload differs |
Correct error code IDEMPOTENCY_KEY_REUSE_MISMATCH |
expect(code).toBe(IDEMPOTENCY_KEY_REUSE_MISMATCH) |
| Same payload returns cached response | same payload with different key order still matches |
| Canonicalization of key order | produces the same hash regardless of key order, same payload with different key order still matches |
| No stored values leaked | does NOT leak stored values |
conflictingSummary fields present |
response includes conflictingSummary... |
| In-progress still works | IDEMPOTENCY_IN_PROGRESS when hash matches but status is started |
conflictingSummaryexposes only SHA-256 fingerprints and sorted field names — never stored field values- No new external dependencies
- Existing stored-value security (server-error key deletion) preserved
closes #427