Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/parser/src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types';
import type { MiddlewareObj } from '@middy/core';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { ParseError } from '../errors.js';
import { parse } from '../parser.js';
import type { Envelope } from '../types/envelope.js';
import type { ParserOptions, ParserOutput } from '../types/parser.js';
Expand Down Expand Up @@ -38,10 +39,20 @@ const parser = <
>(
options: ParserOptions<TSchema, TEnvelope, TSafeParse>
): MiddlewareObj<ParserOutput<TSchema, TEnvelope, TSafeParse>> => {
const before = (request: MiddyLikeRequest): void => {
const { schema, envelope, safeParse } = options;
const before = (request: MiddyLikeRequest): unknown => {
const { schema, envelope, safeParse, errorHandler } = options;

request.event = parse(request.event, envelope, schema, safeParse);
try {
request.event = parse(request.event, envelope, schema, safeParse);
} catch (error) {
if (errorHandler && error instanceof ParseError) {
const result = errorHandler(error);
if (result !== undefined) {
return result;
}
}
throw error;
}
};

return {
Expand Down
41 changes: 37 additions & 4 deletions packages/parser/src/parserDecorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import type { Callback, Context, Handler } from 'aws-lambda';
import { ParseError } from './errors.js';
import { parse } from './parser.js';
import type { Envelope, ParserOptions } from './types/index.js';
import type { ParserOutput } from './types/parser.js';
Expand Down Expand Up @@ -66,6 +67,29 @@ import type { ParserOutput } from './types/parser.js';
* }
* ```
*
* You can also provide an `errorHandler` callback to intercept parse errors and return a custom response
* instead of throwing. If the handler returns `undefined`, the error is rethrown.
*
* @example
* ```typescript
* import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
* import { z } from 'zod';
* import { parser } from '@aws-lambda-powertools/parser';
*
* const Order = z.object({
* orderId: z.string(),
* description: z.string(),
* });
*
* class Lambda implements LambdaInterface {
*
* @parser({ schema: Order, errorHandler: (error) => ({ statusCode: 400, body: error.message }) })
* public async handler(event: z.infer<typeof Order>, _context: Context): Promise<unknown> {
* return processOrder(event);
* }
* }
* ```
*
* @param options Configure the parser with the `schema`, `envelope` and whether to `safeParse` or not
*/
export const parser = <
Expand All @@ -82,15 +106,24 @@ export const parser = <
) => {
const original = descriptor.value;

const { schema, envelope, safeParse } = options;
const { schema, envelope, safeParse, errorHandler } = options;

descriptor.value = function (
this: Handler,
...args: [ParserOutput<TSchema, TEnvelope, TSafeParse>, Context, Callback]
) {
const parsedEvent = parse(args[0], envelope, schema, safeParse);

return original.apply(this, [parsedEvent, ...args.slice(1)]);
try {
const parsedEvent = parse(args[0], envelope, schema, safeParse);
return original.apply(this, [parsedEvent, ...args.slice(1)]);
} catch (error) {
if (errorHandler && error instanceof ParseError) {
const result = errorHandler(error);
if (result !== undefined) {
return result;
}
}
throw error;
}
};

return descriptor;
Expand Down
2 changes: 2 additions & 0 deletions packages/parser/src/types/parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import type { ParseError } from '../errors.js';
import type {
ArrayEnvelope,
DynamoDBArrayEnvelope,
Expand All @@ -17,6 +18,7 @@ type ParserOptions<
schema: TSchema;
envelope?: TEnvelope;
safeParse?: TSafeParse;
errorHandler?: (error: ParseError) => unknown;
};

/**
Expand Down
67 changes: 67 additions & 0 deletions packages/parser/tests/unit/parser.decorator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ describe('Decorator: parser', () => {
return event;
}

@parser({
schema,
errorHandler: (_error) => undefined,
})
public async handlerWithErrorHandlerReturningUndefined(
event: z.infer<typeof schema>,
_context: Context
): Promise<unknown> {
return event;
}

@parser({
schema,
errorHandler: (error) => ({ errorHandled: true, message: error.message }),
})
public async handlerWithErrorHandler(
event: z.infer<typeof schema>,
_context: Context
): Promise<unknown> {
return event;
}

private anotherMethod(event: unknown): unknown {
return event;
}
Expand Down Expand Up @@ -142,4 +164,49 @@ describe('Decorator: parser', () => {
originalEvent: { foo: 'bar' },
});
});

it('rethrows the error when errorHandler returns undefined', () => {
// Act & Assess
expect(() =>
lambda.handlerWithErrorHandlerReturningUndefined(
{ foo: 'bar' } as unknown as z.infer<typeof schema>,
{} as Context
)
).toThrow(ParseError);
});

it('calls the errorHandler when schema validation fails', async () => {
// Act
const result = await lambda.handlerWithErrorHandler(
{ foo: 'bar' } as unknown as z.infer<typeof schema>,
{} as Context
);

// Assess
expect(result).toEqual({
errorHandled: true,
message: expect.any(String),
});
});

it('does not call the errorHandler when schema validation succeeds', async () => {
// Prepare
const event = { name: 'John', age: 30 };

// Act
const result = await lambda.handlerWithErrorHandler(
event as unknown as z.infer<typeof schema>,
{} as Context
);

// Assess
expect(result).toEqual(event);
});

it('rethrows the error when no errorHandler is provided and schema validation fails', () => {
// Act & Assess
expect(() =>
lambda.handler({ foo: 'bar' } as unknown as event, {} as Context)
).toThrow(ParseError);
});
});
71 changes: 71 additions & 0 deletions packages/parser/tests/unit/parser.middy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,75 @@
originalEvent: event,
});
});

it('rethrows the error when errorHandler returns undefined', async () => {
// Prepare
const event = structuredClone(JSONPayload);

// Act & Assess
await expect(
middy()
.use(
parser({
schema: z.number(),
errorHandler: (_error) => undefined,
})
)
.handler((event) => event)(event as unknown as number, {} as Context)

Check warning on line 207 in packages/parser/tests/unit/parser.middy.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-typescript&issues=AZ6_TK4wixFPtcnbDppU&open=AZ6_TK4wixFPtcnbDppU&pullRequest=5352
).rejects.toThrow(ParseError);
});

it('calls the errorHandler and short-circuits when schema validation fails', async () => {
// Prepare
const event = structuredClone(JSONPayload);

// Act
const result = await middy()
.use(
parser({
schema: z.number(),
errorHandler: (error) => ({ errorHandled: true, message: error.message }),
})
)
.handler((event) => event)(event as unknown as number, {} as Context);

Check warning on line 223 in packages/parser/tests/unit/parser.middy.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-typescript&issues=AZ6_TK4wixFPtcnbDppV&open=AZ6_TK4wixFPtcnbDppV&pullRequest=5352

// Assess
expect(result).toEqual({
errorHandled: true,
message: expect.any(String),
});
});

it('does not call the errorHandler when schema validation succeeds', async () => {
// Prepare
const event = structuredClone(JSONPayload);

// Act
const result = await middy()
.use(
parser({
schema: schema,
errorHandler: (error) => ({ errorHandled: true, message: error.message }),
})
)
.handler((event) => event)(
event as unknown as z.infer<typeof schema>,
{} as Context
);

// Assess
expect(result).toEqual(event);
});

it('rethrows the error when no errorHandler is provided and schema validation fails', async () => {
// Prepare
const event = structuredClone(JSONPayload);

// Act & Assess
await expect(
middy()
.use(parser({ schema: z.number() }))
.handler((event) => event)(event as unknown as number, {} as Context)

Check warning on line 261 in packages/parser/tests/unit/parser.middy.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-typescript&issues=AZ6_TK4wixFPtcnbDppW&open=AZ6_TK4wixFPtcnbDppW&pullRequest=5352
).rejects.toThrow(ParseError);
});
});