powertools-lambda-typescript
powertools-lambda-typescript copied to clipboard
Bug: Idempotency middleware doesn't work for functions without return value
Expected Behavior
When using the makeHandlerIdempotent middleware with a handler function that has no return value(undefined), the middleware should only call the handler once.
Current Behavior
The handler is called multiple times for the same event, even though the idempotency record exists in Dynamo.
The issue is that the package returns the stored return value from previous run from the middleware. Here: https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/packages/idempotency/src/IdempotencyHandler.ts#L243 And here: https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/packages/idempotency/src/middleware/makeHandlerIdempotent.ts#L140
If the value returned from middleware is truthy, it causes middy to perform an early return(https://middy.js.org/docs/intro/early-interrupt/).
This works for handler that have a return value, but doesn't work when the return value is undefined - middy doesn't early return and the handler code is executed again.
Code snippet
export handler = middy().use(makeHandlerIdempotent(...)).handler(() => console.log("stuff"))
Steps to Reproduce
Use the code snippet, call the lambda multiple times with the same event
Possible Solution
In order to early return, a truthy value has to be returned from the middleware.
This kind of sucks, because the first time a lambda runs, it returns undefined and we'd have to return something else when idempotency key is present.
In our use case, the return value doesn't matter, but I'm not sure what would make sense as a generic solution.
Powertools for AWS Lambda (TypeScript) version
latest
AWS Lambda function runtime
20.x
Packaging format used
npm
Execution logs
No response
@dreamorosi added support for functions with no return value in https://github.com/aws-powertools/powertools-lambda-typescript/pull/2521
Hope this helps with the triage!
Hi @wassil, thank you for opening the issue and reporting the bug.
I was able to reproduce the behavior you describe and confirm that when the handler function doesn't return a value, the Middy.js middleware runs twice.
We'll look into fixing the issue.
I have spent some time looking at the implementation (thanks for the exhaustive report!) and unfortunately I think we can't do anything unless Middy.js itself supports this use case.
I have opened an issue on their repo (middyjs/middy#1236) and will be waiting to hear from the maintainer(s) on whether this is something that can be supported or not.
I'll wait for a couple of days, after which I'll add a banner to our docs to explain the limitation and suggest customers to use other Idempotency methods (i.e. function wrapper or class method decorator) if their use case can return undefined.
Yeah, there is no nice clean solution and I don't think there is a difference between return undefined and no return in JS.
I was considering writing my own idempotency middleware using this package. We also have a batch processing use-case where I want to mark individual messages as processed. The only way I found was something along the lines of:
let promiseResolver;
{
// start a dummy function that will be made idempotent
before() => { makeIdempotent(() => new Promise(done => promiseResolver = done))(idempotencyKeyFromPayload); }
// finish the dummy function so it's marked complete in the idempotency table
after() => { promiseResolver(); }
// throw an error so the idempotency entry is cleared from the table
onError() => { promiseResolver(new Error("BOOM!"))}
}
But it feels like a hack to be honest.
Have you considered exposing more of the functionality from the package so it's easier to build these special cases?
Basically something like "I have started processing", "Processing completed" and "Error during processing" functions.
All of these are available in IdempotencyHandler, but I understand that you want to keep that private.
We have something similar as a feature request that is already in python implementation #2887 but this is mostly for "Processing completed" case.
Middy documentation is now updated.
Have you considered exposing more of the functionality from the package so it's easier to build these special cases?
We haven't and in general we try to keep our public API tidy & focused, although we are open to proposals in this sense.
For this specific case, I'd like to propose the following actions:
- Update the Powertools for AWS docs to clarify this limitation when using the Middy.js
makeHandlerIdempotentdecorator - Follow and monitor v6 development for Middy.js and see if they can make changes that would allow this type of feature. There were some proposals in the linked issue that might yield a solution and allow the use case without relying on early returns. If this is the case and the change lands in v6 (or v7) we'd recommend Powertools for AWS customers wanting to use the
makeHandlerIdempotentmiddleware with functions that can returnundefinedto upgrade to said version - Recommend customers with this use case to use one of the alternate methods Powertools for AWS offers.
Specifically, even when using other Middy.js middlewares, you can wrap your handler like this:
import middy from '@middy/core';
import { Logger } from '@aws-lambda-powertools/logger';
import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware';
import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
const logger = new Logger();
const persistenceStore = new DynamoDBPersistenceLayer({
tableName: process.env.IDEMPOTENCY_TABLE_NAME as string,
});
export const handler = middy(
makeIdempotent(
async () => {
logger.info('stuff');
},
{ persistenceStore }
)
).use(injectLambdaContext(logger));
In this example I am using another Powertools for AWS middleware (from Logger), but using the makeIdempotent wrapper function around the handler and then passing this to @middy/core roughly equates to using the makeHandlerIdempotent Middy.js-compatible middleware as first in the stack.
The difference however is that using this method your function is considered idempotent even if its return value is undefined. This is because we handle the logic internally in the Idempotency utility.
I will open a PR to update the docs shortly.
⚠️ COMMENT VISIBILITY WARNING ⚠️
This issue is now closed. Please be mindful that future comments are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.
This is now released under v2.8.0 version!