You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The middleware did GET(miss) → run handler → SET with no atomic reservation
between the GET and the SET. Two requests carrying the same Idempotency-Key
(or the same body-fingerprint) racing in that window both saw redis.Nil and
both ran the handler — double-creating real backend resources on the
authenticated /db|/cache|/nosql provision paths, which have no other
per-burst dedup gate (the per-fingerprint INCR cap lives only on the anon
branch; CreateDeployment already has its own FOR UPDATE cap).
Fix: on a cache miss, write an in-flight reservation marker via SETNX
(idemEntry.InFlight, 60s TTL) the instant the handler starts. A concurrent
same-key request reads the marker in the top GET hit-block and returns 409
idempotency_key_in_progress (retryable — same envelope as the existing
conflict 409) instead of re-running the handler. The real response overwrites
the marker on completion (plain SET); non-cacheable paths (5xx, handler
error, mutable 4xx) DELETE the marker so an immediate legitimate retry isn't
blocked. Applied symmetrically to both the explicit-key and fingerprint
branches. SETNX is best-effort/fail-open — a Redis hiccup never blocks
provisioning (documented dedup posture, never a correctness gate).
Tests: TestIdempotency_{ExplicitKey,Fingerprint}_ConcurrentInFlight_Returns409
hold request A in the handler (blocked on a channel) so B observes A's live
reservation → 409, handler runs exactly once. Pass under -race; new code 100%
covered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"message": "A request with this Idempotency-Key is still being processed.",
356
+
"request_id": GetRequestID(c),
357
+
"retry_after_seconds": 2,
358
+
"agent_action": "Another request with the same Idempotency-Key is still in flight. Do NOT mint a new key — wait ~2s and retry the SAME key; the original response will be replayed once it completes. See https://instanode.dev/docs/idempotency.",
359
+
})
360
+
}
361
+
362
+
// reserveInflight best-effort places an in-flight marker at cacheKey via SETNX,
363
+
// so a concurrent same-key request that arrives after this point reads the
364
+
// marker and returns 409 in_progress instead of double-running the handler
365
+
// (BugBash 2026-06-02 #21). Fire-and-forget: a SETNX error or a lost sub-
366
+
// millisecond race just means the marker isn't placed, and the request falls
367
+
// back to the documented best-effort dedup posture (run the handler; the GET
368
+
// hit-block still catches the common case where the other request reserved
369
+
// first). The marker is overwritten by the real entry on completion, or
0 commit comments