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 record acquired at the start of an idempotent operation should be the record that is completed on success or deleted on failure. The key and validation hash should be fixed when the operation acquires its record, regardless of what the wrapped function does to its arguments afterwards.
Current Behavior
IdempotencyHandler keeps a reference to the payload it hashes, and BasePersistenceLayer recomputes the idempotency key and validation hash from that payload on every call. If the wrapped function mutates its input, for example by normalising fields or adding a processed flag, saveInProgress() hashes the original payload and saveSuccess() or deleteRecord() hash the mutated one, so they target a different key.
Consequences with the default whole-event hash:
On success, the completed result is written under a key no caller will ever look up. The original record stays INPROGRESS. A retry with the same event throws IdempotencyAlreadyInProgressError until the in-progress deadline passes, and once the record expires the side effect can run again.
On failure, cleanup deletes the wrong key and the original in-progress record is left behind until it expires.
With payload validation enabled, mutating a validated field changes the hash stored at completion, so an identical retry can fail validation.
This applies to makeIdempotent, the decorator, and the Middy middleware, since all three pass the event object by reference. Mutating the event in a handler is common enough that the framework should not depend on callers avoiding it.
Code snippet
importassertfrom'node:assert/strict';import{IdempotencyConfig,makeIdempotent,}from'@aws-lambda-powertools/idempotency';import{DynamoDBPersistenceLayer}from'@aws-lambda-powertools/idempotency/dynamodb';constpersistenceStore=newDynamoDBPersistenceLayer({tableName: process.env.IDEMPOTENCY_TABLE_NAME??'idempotency',});constprocessOrder=makeIdempotent(async(order: {id: string;amount: number;normalized?: boolean})=>{order.normalized=true;// mutates the object used for hashingreturn{processed: order.id};},{
persistenceStore,config: newIdempotencyConfig({expiresAfterSeconds: 3600}),});constfirst=awaitprocessOrder({id: 'order-1',amount: 10});// Same event again should replay the stored result.// Currently throws IdempotencyAlreadyInProgressError: the completed result was// saved under the hash of { id, amount, normalized: true }, while the record// for { id, amount } is still INPROGRESS.constsecond=awaitprocessOrder({id: 'order-1',amount: 10});assert.deepEqual(second,first);
Steps to Reproduce
Wrap a function with makeIdempotent using the default configuration, so the whole event is hashed.
Inside the function, add or change a property on the event object.
Invoke it once, then inspect the table: one record is INPROGRESS under the hash of the original event, and a COMPLETED record exists under the hash of the mutated event.
Invoke it again with an identical event. It throws IdempotencyAlreadyInProgressError instead of returning the stored result.
As a control, have the function clone the event before mutating it. Both invocations return the same result and only one record exists.
Possible Solution
Bind record identity to the operation rather than recomputing it on every persistence call. Two approaches:
Compute the idempotency key and validation hash once in IdempotencyHandler when the record is acquired, and pass them to the subsequent saveSuccess() and deleteRecord() calls. This is the same direction as Bug: Idempotency operations sharing a persistence store complete under the wrong key prefix #5707, which pinned the key prefix per operation, and would let the prefix re-application added there be replaced by an explicit key.
Alternatively, snapshot the payload before invoking the wrapped function, for example with structuredClone, and hash the snapshot throughout. Simpler, but it copies every event and does not cover the shared-prefix case.
Expected Behavior
The record acquired at the start of an idempotent operation should be the record that is completed on success or deleted on failure. The key and validation hash should be fixed when the operation acquires its record, regardless of what the wrapped function does to its arguments afterwards.
Current Behavior
IdempotencyHandlerkeeps a reference to the payload it hashes, andBasePersistenceLayerrecomputes the idempotency key and validation hash from that payload on every call. If the wrapped function mutates its input, for example by normalising fields or adding aprocessedflag,saveInProgress()hashes the original payload andsaveSuccess()ordeleteRecord()hash the mutated one, so they target a different key.Consequences with the default whole-event hash:
INPROGRESS. A retry with the same event throwsIdempotencyAlreadyInProgressErroruntil the in-progress deadline passes, and once the record expires the side effect can run again.This applies to
makeIdempotent, the decorator, and the Middy middleware, since all three pass the event object by reference. Mutating the event in a handler is common enough that the framework should not depend on callers avoiding it.Code snippet
Steps to Reproduce
makeIdempotentusing the default configuration, so the whole event is hashed.INPROGRESSunder the hash of the original event, and aCOMPLETEDrecord exists under the hash of the mutated event.IdempotencyAlreadyInProgressErrorinstead of returning the stored result.Possible Solution
Bind record identity to the operation rather than recomputing it on every persistence call. Two approaches:
IdempotencyHandlerwhen the record is acquired, and pass them to the subsequentsaveSuccess()anddeleteRecord()calls. This is the same direction as Bug: Idempotency operations sharing a persistence store complete under the wrong key prefix #5707, which pinned the key prefix per operation, and would let the prefix re-application added there be replaced by an explicit key.structuredClone, and hash the snapshot throughout. Simpler, but it copies every event and does not cover the shared-prefix case.The first is recommended.
Powertools for AWS Lambda (TypeScript) version
2.35.0
AWS Lambda function runtime
22.x
Packaging format used
npm