Conversation
There was a problem hiding this comment.
Can you please align your README.md with the one listed here: https://github.com/aws-samples/serverless-patterns/tree/main/_pattern-model
- Order of items
- How It Works, Webhook Processing Workflow (3 Steps), Key Features can be condensed in one item
- Required IAM Permissions - can be removed
- Components section can be removed entirely
- API Endpoints, Monitoring, Error Handling, Cost Optimization, Security Considerations, NodeJS Implementation Notes can be removed
- Learn More can be integrated into the first description using Links to the docs
- The README does not mention that deployed resources may incur AWS charges. While the pattern uses pay-per-use services, a cost warning helps users understand they should clean up after testing.
- The README does not mention that deployed resources may incur AWS charges. While the pattern uses pay-per-use services, a cost warning helps users understand they should clean up after testing.
If you like, you
- can add numbers to your architecture diagram and move/re-use content blocks (please as short/crisp as possible) to the architecture diagram (below) in a written form.
| sam build | ||
| ``` | ||
|
|
||
| 2. **Deploy to AWS**: |
There was a problem hiding this comment.
Please note required inputs WebhookSecret
| - `WebhookApiUrl`: Use this for sending webhook POST requests | ||
| - `StatusQueryApiUrl`: Use this for querying execution status | ||
|
|
||
| 3. **Test the webhook**: |
There was a problem hiding this comment.
| 3. **Test the webhook**: | |
| ## Testing |
Own section please
| curl <StatusQueryApiUrl> | ||
| ``` | ||
|
|
||
| **Success indicators:** |
There was a problem hiding this comment.
Please ad a way testing the failure handling & timeout (set to a low limit) as it is described as a key capability of the sample
| - Effect: Allow | ||
| Action: | ||
| - lambda:CheckpointDurableExecutions | ||
| - lambda:GetDurableExecutionState | ||
| Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${Environment}-webhook-processor' |
There was a problem hiding this comment.
| - Effect: Allow | |
| Action: | |
| - lambda:CheckpointDurableExecutions | |
| - lambda:GetDurableExecutionState | |
| Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${Environment}-webhook-processor' | |
| - AWSLambdaBasicExecutionRole | |
| - AWSLambdaBasicDurableExecutionRolePolicy |
Should be fine to be replaced with managed policies
| })); | ||
|
|
||
| // Call the separate webhook validator function | ||
| const { LambdaClient, InvokeCommand } = await import('@aws-sdk/client-lambda'); |
There was a problem hiding this comment.
Two issues here:
Dynamic import inside a step: The await import() is an async operation that adds latency on every first execution of this step. If the step retries, the import runs again. SDK clients should be initialized in the module scope (outside the handler) for connection reuse across warm invocations.
New client on every step execution: Creating a new LambdaClient inside the step means no HTTP connection reuse. The DynamoDB client is correctly initialized at module scope (lines 7-8), but the Lambda client is not.
// ✅ FIX — Move to module scope alongside DynamoDB client
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
const lambdaClient = new LambdaClient({});
| const validatorFunctionArn = process.env.WEBHOOK_VALIDATOR_FUNCTION_ARN; | ||
| const invokeResponse = await lambdaClient.send(new InvokeCommand({ | ||
| FunctionName: validatorFunctionArn, | ||
| Payload: JSON.stringify({ | ||
| payload: webhookPayload, | ||
| executionToken: executionToken | ||
| }) | ||
| })); |
There was a problem hiding this comment.
The durable execution SDK provides context.invoke() specifically for calling other Lambda functions from within a durable function. Using context.invoke() instead of manual SDK invocation provides:
Automatic checkpointing of the invocation result
Built-in retry handling
Proper integration with the durable execution lifecycle
The current approach wraps a manual Lambda invocation inside a step, which works but misses the SDK's built-in invocation support.
// ✅ FIX — Use context.invoke() for Lambda-to-Lambda calls
const validatorResult = await context.invoke(validatorFunctionArn, {
payload: webhookPayload,
executionToken: executionToken
});
| } catch (error) { | ||
| console.error(`Error processing webhook ${executionToken}:`, error.message); | ||
|
|
||
| // Update error state | ||
| await dynamodb.send(new UpdateCommand({ | ||
| TableName: eventsTableName, | ||
| Key: { executionToken }, | ||
| UpdateExpression: 'SET #status = :status, #error = :error', | ||
| ExpressionAttributeNames: { | ||
| '#status': 'status', | ||
| '#error': 'error' | ||
| }, | ||
| ExpressionAttributeValues: { | ||
| ':status': 'FAILED', | ||
| ':error': error.message | ||
| } | ||
| })); |
There was a problem hiding this comment.
The catch block is outside any step. If an error occurs and the function retries, the catch block's DynamoDB write runs on every retry attempt. More critically, if the error is transient and the function eventually succeeds on retry, the catch block from the failed attempt has already written FAILED status to DynamoDB — but the successful retry overwrites it. This creates a race condition in the status visible to the query API.
The durable SDK handles retries automatically for steps. The outer try/catch should only handle truly unrecoverable errors, and the error state write should be wrapped in a step.
// ✅ FIX — Let the SDK handle retries; wrap error handling in a step
} catch (error) {
const errorResult = await context.step('handle-error', async () => {
await dynamodb.send(new UpdateCommand({
TableName: eventsTableName,
Key: { executionToken },
UpdateExpression: 'SET #status = :status, #error = :error',
ExpressionAttributeNames: { '#status': 'status', '#error': 'error' },
ExpressionAttributeValues: { ':status': 'FAILED', ':error': error.message }
}));
return { status: 'FAILED', error: error.message };
});
return {
statusCode: 500,
body: JSON.stringify({ message: 'Webhook processing failed', ...errorResult })
};
}
| return { | ||
| executionToken: executionToken, | ||
| status: "processed", | ||
| originalPayload: webhookPayload, | ||
| businessResult: `Processed webhook of type: ${webhookPayload.type || 'unknown'}`, | ||
| dataTransformed: webhookPayload.data ? JSON.stringify(webhookPayload.data).toUpperCase() : null, | ||
| processedAt: new Date().toISOString(), | ||
| metadata: { | ||
| processedBy: 'webhook-processor-nodejs', | ||
| version: '1.0.0' | ||
| } | ||
| }; | ||
| }); |
There was a problem hiding this comment.
| return { | |
| executionToken: executionToken, | |
| status: "processed", | |
| originalPayload: webhookPayload, | |
| businessResult: `Processed webhook of type: ${webhookPayload.type || 'unknown'}`, | |
| dataTransformed: webhookPayload.data ? JSON.stringify(webhookPayload.data).toUpperCase() : null, | |
| processedAt: new Date().toISOString(), | |
| metadata: { | |
| processedBy: 'webhook-processor-nodejs', | |
| version: '1.0.0' | |
| } | |
| }; | |
| }); | |
| return { | |
| executionToken, | |
| status: "processed", | |
| payloadType: webhookPayload.type || 'unknown', | |
| processedAt: new Date().toISOString() | |
| }; |
AWS best practices state:
"Keep state minimal — Store IDs and references, not full objects. [...] Use Amazon S3 or DynamoDB for large data, pass references in state."
The originalPayload is already stored in DynamoDB (from the initial PutCommand). Including it in the step return value means it's also serialized into the checkpoint. For large webhook payloads, this doubles storage and increases checkpoint costs. The processingResult is then also embedded in Step 3's DynamoDB write and return value, tripling the data.
There was a problem hiding this comment.
Please review
Each step writes a status update to DynamoDB before doing its main work. If a step retries, the DynamoDB UpdateCommand runs again. While DynamoDB UpdateCommand is naturally idempotent for these status writes (writing the same value is a no-op), the pattern should be documented as intentionally idempotent.
More importantly, if Step 1 completes but Step 2 fails and the function replays, Step 1's DynamoDB write does NOT re-execute (it's checkpointed), but the DynamoDB record still shows the status from Step 1's original execution. This is correct behavior, but the pattern should be aware that the DynamoDB status reflects the last successfully written status, not necessarily the current replay position.
No code change needed, but this should be documented in the README as a design consideration.
Issue #, if available:
Description of changes:
adding nodejs webhook df pattern
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.