Idempotency-Key middleware support for /api/exports/schedules endpoints:
POST /api/exports/schedules— create a new export schedulePATCH /api/exports/schedules/:scheduleId— update an existing schedule
Provides safe retry capabilities for export mutations.
The function parameter config shadowed the module-level config import from ../config/index.js. When Express called the middleware as (req, res, next), the parameter was undefined, causing config.idempotency.retentionWindowSeconds to throw TypeError: Cannot read properties of undefined. Fixed by:
- Renaming the parameter from
configtoopts - Using
opts?.retentionSeconds ?? config.idempotency.retentionWindowSeconds(module-level fallback)
The EXPORT_IDEMPOTENCY_CONFIG constant was defined but never passed to the middleware. Created a wrapper handler that passes the config:
const idempotencyHandler = (req, res, next) =>
idempotencyMiddleware(req, res, next, EXPORT_IDEMPOTENCY_CONFIG);Idempotent-Replayed: trueheader on replayed responses- 409
IDEMPOTENCY_KEY_REUSE_MISMATCH— idempotency key reused with different payload - 409
IDEMPOTENCY_IN_PROGRESS— concurrent duplicate request - Conflict body:
{ error, message, code, conflictingSummary: { idempotencyKey, incomingPayloadFingerprint, storedPayloadFingerprint, incomingFields } }
- Fixed
makeReqhelper: used'key' in overridesinstead of destructuring defaults (defaults applied even whenundefinedwas explicitly passed, making it impossible to signal "no header") - Fixed
makeDbhelper: added a thirdmockResolvedValueOncefor the SELECT query (middleware makes 2x DELETE + 1x SELECT before INSERT/UPDATE) - Fixed combined DELETE assertion to match actual two-query implementation
Added 4 integration tests for idempotency on export mutations:
- POST with idempotency key replays on retry
- PATCH with idempotency key replays on retry
- POST with mismatched payload returns 409
- POST without idempotency key still succeeds
Fixed error envelope assertion: response.body.code → response.body.error.code (correct path for the standardized error envelope).
- Supports
Idempotency-Keyheader oridempotencyKeybody field - SHA-256 fingerprint of
{ userId, method, path, sorted body minus idempotencyKey } - Canonicalization: stable key ordering for consistent hashing
- Replays cached 2xx/4xx responses; deletes key on 5xx for safe retry
- 409 on payload mismatch with fingerprint summary (no sensitive data leaked)
- 409 on in-progress status for concurrent duplicates
| Method | Path | Description |
|---|---|---|
| POST | /api/exports/schedules |
Create schedule (idempotent) |
| PATCH | /api/exports/schedules/:scheduleId |
Update schedule (idempotent) |
| GET | /api/exports/schedules |
List schedules (no idempotency) |
409 Conflict errors returned directly by middleware (not through shared error handler):
- Payload mismatch →
IDEMPOTENCY_KEY_REUSE_MISMATCH - In-progress →
IDEMPOTENCY_IN_PROGRESS
All other errors use the standard error handler chain.
- Keys stored with fingerprint verification; no raw payload retention
- Conflict summaries expose only top-level field names (no values)
- Body fields in
bodyExcludingKeys(e.g.idempotencyKey) stripped before hashing - 5xx errors delete the key so clients can safely retry
- 26 tests pass: 20 idempotency middleware unit tests + 6 export routes integration tests
- Idempotency middleware: 100% coverage of cache-hit, cache-miss, mismatch, in-progress, error paths
- Export schedules: functional tests for create, update (invalid cron), idempotency replay, conflict, no-key passthrough