Skip to content

Add Idempotency-Key support on /api/health mutations - #881

Merged
greatest0fallt1me merged 1 commit into
Predictify-org:mainfrom
arisu6804:feat/health-idempotency-665
Jul 30, 2026
Merged

Add Idempotency-Key support on /api/health mutations#881
greatest0fallt1me merged 1 commit into
Predictify-org:mainfrom
arisu6804:feat/health-idempotency-665

Conversation

@arisu6804

Copy link
Copy Markdown
Contributor

Closes #665

Summary

POST /api/health/mutations writes 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 /api POST/PATCH routes in src/index.ts — but that global registration happens after app.use("/api/health", healthRouter), so it never actually runs for this route (Express matches middleware in registration order; the request is fully handled by healthRouter before ever reaching the later idempotency mount).

Fix: apply idempotency directly as route-level middleware on POST /mutations in src/routes/health.ts, so retried requests with the same Idempotency-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 the db mock (mirroring the existing tests/authIdempotency.test.ts pattern) 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 that requestTimeout's req.on("close", () => abort()) handler fires under supertest before the response finishes, causing abortableRace to reject in-flight promises — this reproduces in total isolation (a two-line Express app with just requestTimeout + abortableRace, no idempotency or health-route code involved) and is unrelated to this change (confirmed git diff main -- src/middleware/timeout.ts is empty). It's out of scope for this issue since requestTimeout is shared by most routes in this codebase and warrants its own investigation; I stubbed both requestTimeout and abortableRace as 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-existing src/routes/users.ts syntax errors present identically on main.
  • npx eslint src/routes/health.ts tests/healthIdempotency.test.ts — clean, no errors.

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
@greatest0fallt1me
greatest0fallt1me merged commit e19fbc2 into Predictify-org:main Jul 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Idempotency-Key support on /api/health mutations [b#091]

2 participants