Skip to content

fix(idempotency): delete only acquired records in the Middy onError hook - #5676

Merged
svozza merged 1 commit into
mainfrom
fix/idempotency-middy-on-error-delete
Sep 9, 2026
Merged

svozza merged 1 commit into
mainfrom
fix/idempotency-middy-on-error-delete

Conversation

@svozza

@svozza svozza commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

The makeHandlerIdempotent middleware's onError hook runs whenever any earlier step throws, including its own before hook. It deleted the idempotency record unconditionally, so a rejected duplicate (IdempotencyAlreadyInProgressError) removed the lock of the still-running original invocation, and a rejected payload mismatch (IdempotencyValidationError) removed the original request's completed record. Either way a subsequent retry executed the handler again. Reproduced on Lambda + DynamoDB; the makeIdempotent wrapper is unaffected because its try/catch only wraps the function call.

The middleware now tracks whether the current request acquired an unfinished in-progress record and deletes only in that case.

Changes

  • before marks the request as owning the record when handleMiddyBefore did not replay a stored response, detected via the replay cleanup callback. Both per-request flags are reset at the start of before, since Middy reuses a user-supplied internal object across warm invocations; this also closes the same stale-flag leak in the existing .skip flag.
  • after releases ownership before saving the result, so a saveSuccess failure no longer deletes the record, in line with the wrapper.
  • onError returns early unless the request owns the record.
  • Docs: "Handling exceptions" states when the middleware keeps the record and when it still deletes it.
  • Tests: six new cases in makeIdempotent.test.ts covering the concurrent duplicate, payload mismatch, undefined stored response, later after failure, shared internal storage, and a later before failure that must still delete; the saveSuccess failure case now asserts no delete.

Design note

Ownership is tracked in the middleware via request.internal, next to the existing .skip flag, rather than as a field on IdempotencyHandler. A core-class flag would be a smaller diff (set after saveInProgress resolves, cleared on saveSuccess, checked in handleMiddyOnError), but the problem is specific to Middy's lifecycle: onError fires for failures in before, which the wrapper and decorator paths never encounter. Keeping the state in the middleware leaves IdempotencyHandler untouched and keeps all per-request Middy state in one place.

Issue number: closes #5675


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.

The middleware's onError hook runs whenever an earlier step throws,
including its own before hook. It deleted the record unconditionally, so a
rejected duplicate removed the lock of the still-running invocation and a
rejected payload mismatch removed the original completed record.

Track in the Middy request whether this invocation acquired an unfinished
in-progress record and delete only in that case.

Closes #5675
@svozza
svozza merged commit 9267cb3 into main Sep 9, 2026
43 checks passed
@svozza
svozza deleted the fix/idempotency-middy-on-error-delete branch September 9, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L PRs between 100-499 LOC

2 participants