Description
What were you searching in the docs?
There is a recommendation for esbuild and the CDK which excludes some packages when bundling function code.
new NodejsFunction(this, 'Function', {
...
bundling: {
externalModules: [
'@aws-lambda-powertools/*',
'@aws-sdk/*',
],
}
});
This can cause version mismatches and cause unexpected behaviour in code if versions are not managed properly. Namely, when using instanceof
as a condition, the result will always be false
if the Lambda Layer version and package version do not match.
As an example, take this function definition deployed through the CDK:
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Logger } from '@aws-lambda-powertools/logger';
import { ParseError, parser } from '@aws-lambda-powertools/parser';
import { Tracer } from '@aws-lambda-powertools/tracer';
import { APIGatewayProxyEventSchema } from '@aws-lambda-powertools/parser/schemas';
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
import { z, ZodError, ZodIssue } from 'zod';
const logger = new Logger();
const tracer = new Tracer();
export const requestSchema = APIGatewayProxyEventSchema.extend({
queryStringParameters: z.object({
message: z.string().max(10),
}),
});
const handleZodError = (error: ZodError) => {
const flattenedErrors = error.flatten((issue: ZodIssue) => ({
field: issue.path.join('.'),
issue: issue.message,
}));
return {
statusCode: 422,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: 'Validation error',
data: { validationErrors: flattenedErrors.fieldErrors },
}),
};
};
export function httpErrorHandler(): MethodDecorator {
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
if (error instanceof Error) {
logger.error('Error in API handler', error);
tracer.addErrorAsMetadata(error);
} else {
logger.error('Unknown error in API handler', { error });
}
if (error instanceof ParseError && error.cause instanceof ZodError) {
return handleZodError(error.cause);
}
if (error instanceof ZodError) {
return handleZodError(error);
}
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Internal server error' }),
};
}
};
return descriptor;
};
}
class Lambda implements LambdaInterface {
@httpErrorHandler()
@tracer.captureLambdaHandler({ captureResponse: true })
@logger.injectLambdaContext({ logEvent: true })
@parser({ schema: requestSchema })
public async handler(event: z.infer<typeof requestSchema>, _context: Context): Promise<APIGatewayProxyResult> {
logger.info('Processing item', { item: event.queryStringParameters.message });
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Hello, world!' }),
};
}
}
const myFunction = new Lambda();
export const handler = myFunction.handler.bind(myFunction);
Taking the parser component at version 2.20.0
, we can see that the Lambda Layer version is supposed to be 27:
$ aws ssm get-parameter --name /aws/service/powertools/typescript/generic/all/2.20.0 --region eu-west-1
{
"Parameter": {
...
"Value": "arn:aws:lambda:eu-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:27",
...
}
}
Taking /test?message=testing126123gds
as an example request, different behaviour is observed if different layer versions are used.
Parser Package Version | Layer Version | esbuild externalModules | Result |
---|---|---|---|
2.20.0 | 26 | ['@aws-sdk/*'] | 422 Validation Error (correct) |
2.20.0 | 26 | ['@aws-lambda-powertools/', '@aws-sdk/'] | 500 Internal Server Error ("wrong") |
2.20.0 | 27 | ['@aws-sdk/*'] | 422 Validation Error (correct) |
2.20.0 | 27 | ['@aws-lambda-powertools/', '@aws-sdk/'] | 422 Validation Error (correct) |
This can become especially difficult to manage when installing components at different times. For example, let's say you install Parser one day with yarn add @aws-lambda-powertools/parser
and then at a later date, you install the @aws-lambda-powertools/tracer
component. You're now in a position where not only the packages might be misaligned but also the Lambda Layers required.
Is this related to an existing documentation section?
How can we improve?
I was originally going to report this as a bug, however, it's probably expected really. You can't expect to use different versions of packages and get the same result.
Whilst it's possible to write a dirty unit test in my project to make sure the package versions align, fetching the layer version for a particular version requires AWS credentials which I don't want to generate every time I have to run tests locally. Therefore, I'm just going to rely on API tests to pick this up and stick a big notice in the readme. Not ideal but will probably do okay. Maybe it's possible to do this if the parameter was a publicly accessible parameter?
Got a suggestion in mind?
I think the best course of action would be to simply update the documentation where it talks about excluding modules from esbuild output. I think the best result we can expect is a clear notice that all the versions have to be aligned and the Lambda Layer has to align to that version.
Acknowledgment
- I understand the final update might be different from my proposed suggestion, or refused.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status