Add Idempotency-Key support on /api/health mutations - #881
Merged
greatest0fallt1me merged 1 commit intoJul 30, 2026
Merged
Conversation
POST /api/health/mutations writes an audit-log row recording a before/after state transition on every call. A client retry after a network blip (e.g. a timed-out response whose write actually succeeded) would silently create a duplicate mutation record with no way to detect it. The global Idempotency-Key middleware (src/index.ts) is registered after the /api/health mount, so it never runs for this route. Applies it directly as route-level middleware on POST /mutations instead, so retried requests replay the original response rather than re-executing the mutation. Closes Predictify-org#665
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #665
Summary
POST /api/health/mutationswrites an audit-log row on every call, recording a before/after state transition (mode/maintenance). A client retry after a network blip (e.g. a response that timed out client-side after the write actually succeeded server-side) silently creates a duplicate mutation record, with no way to detect or dedupe it.This repo already has a general-purpose Idempotency-Key middleware (
src/middleware/idempotency.ts), applied globally to/apiPOST/PATCH routes insrc/index.ts— but that global registration happens afterapp.use("/api/health", healthRouter), so it never actually runs for this route (Express matches middleware in registration order; the request is fully handled byhealthRouterbefore ever reaching the lateridempotencymount).Fix: apply
idempotencydirectly as route-level middleware onPOST /mutationsinsrc/routes/health.ts, so retried requests with the sameIdempotency-Key+ body replay the original response instead of re-executing the mutation (and re-writing the audit log). Requests without the header are unaffected — the middleware is a no-op when no key is present.Tests
Added
tests/healthIdempotency.test.ts(5 cases): replay on a repeated key+body (audit log written exactly once across two identical requests), 409 conflict when the same key is reused with a different body, 400 for a malformed key, independent processing when no key header is sent, and a schema sanity check. Uses a small stateful in-memory map for thedbmock (mirroring the existingtests/authIdempotency.test.tspattern) so persist-then-replay round trips can be exercised without a real database.Note on
src/middleware/timeout.ts: while writing these tests I found thatrequestTimeout'sreq.on("close", () => abort())handler fires undersupertestbefore the response finishes, causingabortableRaceto reject in-flight promises — this reproduces in total isolation (a two-line Express app with justrequestTimeout+abortableRace, no idempotency or health-route code involved) and is unrelated to this change (confirmedgit diff main -- src/middleware/timeout.tsis empty). It's out of scope for this issue sincerequestTimeoutis shared by most routes in this codebase and warrants its own investigation; I stubbed bothrequestTimeoutandabortableRaceas pass-throughs in this test file so the idempotency suite isn't flaky because of it.Verification
npx jest tests/healthIdempotency.test.ts— 5/5 pass.npx tsc --noEmit(project config): only the pre-existingsrc/routes/users.tssyntax errors present identically onmain.npx eslint src/routes/health.ts tests/healthIdempotency.test.ts— clean, no errors.