Skip to content

fix(server): cache strict-mode VALIDATION_ERROR to break retry-storm loop (#758)#798

Merged
bokelley merged 1 commit into
mainfrom
bokelley/issue-758
Apr 22, 2026
Merged

fix(server): cache strict-mode VALIDATION_ERROR to break retry-storm loop (#758)#798
bokelley merged 1 commit into
mainfrom
bokelley/issue-758

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • Closes idempotency: strict-mode VALIDATION_ERROR replay loop (unbounded re-execution) #758. Under the strict response-validation default (shipped in feat(server): strict response validation default in dev/test (#727) #757), a drifted handler returned VALIDATION_ERROR and released its idempotency claim — letting a retrying buyer re-enter the handler indefinitely with the same drift. This PR caches the VALIDATION_ERROR envelope under the existing (principal, key, payloadHash) tuple for 10 seconds so same-key retries short-circuit to the cached error instead of re-running side effects.
  • New IdempotencyStore.saveTransientError(...) is optional on the interface — custom store implementations that omit it fall back to release() (pre-idempotency: strict-mode VALIDATION_ERROR replay loop (unbounded re-execution) #758 behavior). Stores built via createIdempotencyStore pick it up automatically, so existing users get the fix for free.
  • Replay path now skips injectReplayed(true) on cached error envelopes — replayed is a success-replay field.
  • Scope inherits the success-cache scope: different-payload retries still hit IDEMPOTENCY_CONFLICT, and buyers generating fresh keys per retry are not short-circuited (backoff is the correct control point there).

Why this shape

See gh issue view 758 for the option matrix. Option 2 (short-TTL error cache) won over Option 1 (circuit breaker) because (a) same-key retry storms are the dominant failure mode under strict mode, (b) it reuses existing scope infrastructure, and (c) it avoids a new per-tenant/per-handler counter abstraction.

Code-reviewer and security-reviewer passes were run on this diff and their Should Fix items are addressed in this PR:

  • Optional method on interface + fallback (code review) — keeps patch semver honest.
  • Concurrency test: parallel retry during handler execution sees SERVICE_UNAVAILABLE (in-flight), not re-execution (code review).
  • DoS note in JSDoc: "alert on sustained VALIDATION_ERROR rates per principal" — the drifted-handler-plus-hostile-buyer pair is a cache-fill vector that warrants monitoring (security review).
  • Dev-experience TTL note: after deploying a handler fix, same-key retries within 10s still replay the cached error; use a fresh key to bypass (security review).

Test plan

  • npm test — 5245 pass / 0 fail
  • New tests in test/server-idempotency.test.js:
    • same-key retry short-circuits to cached VALIDATION_ERROR (handler runs once)
    • different-payload retry still gets IDEMPOTENCY_CONFLICT
    • parallel retry during handler execution sees SERVICE_UNAVAILABLE (in-flight)
    • warn mode is unaffected (no transient-error cache populated)
  • Changeset: patch (.changeset/idempotency-transient-error-cache.md)

🤖 Generated with Claude Code

…loop (#758)

Under the strict response-validation default, a drifted handler used
to release its idempotency claim on the way out, letting a retrying
buyer re-enter the handler indefinitely with the same drift. Dispatcher
now caches the VALIDATION_ERROR envelope under (principal, key, payloadHash)
for 10s so same-key retries short-circuit instead of re-executing.

IdempotencyStore.saveTransientError is optional — custom stores fall
back to release() when absent, preserving the pre-758 behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 8d58987 into main Apr 22, 2026
9 checks 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.

idempotency: strict-mode VALIDATION_ERROR replay loop (unbounded re-execution)

1 participant