From 88e44fa55e90ae790cce379665c099869968952c Mon Sep 17 00:00:00 2001 From: Zelys Date: Fri, 12 Jun 2026 20:49:37 -0500 Subject: [PATCH] feat(parser): add errorHandler option for inline parse error handling --- packages/parser/src/middleware/index.ts | 17 ++++- packages/parser/src/parserDecorator.ts | 41 +++++++++-- packages/parser/src/types/parser.ts | 2 + .../tests/unit/parser.decorator.test.ts | 67 +++++++++++++++++ .../parser/tests/unit/parser.middy.test.ts | 71 +++++++++++++++++++ 5 files changed, 191 insertions(+), 7 deletions(-) diff --git a/packages/parser/src/middleware/index.ts b/packages/parser/src/middleware/index.ts index 064e47c855..eb4d9d5c46 100644 --- a/packages/parser/src/middleware/index.ts +++ b/packages/parser/src/middleware/index.ts @@ -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'; @@ -38,10 +39,20 @@ const parser = < >( options: ParserOptions ): MiddlewareObj> => { - 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 { diff --git a/packages/parser/src/parserDecorator.ts b/packages/parser/src/parserDecorator.ts index d0166a4518..97c99e9995 100644 --- a/packages/parser/src/parserDecorator.ts +++ b/packages/parser/src/parserDecorator.ts @@ -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'; @@ -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, _context: Context): Promise { + * return processOrder(event); + * } + * } + * ``` + * * @param options Configure the parser with the `schema`, `envelope` and whether to `safeParse` or not */ export const parser = < @@ -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, 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; diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts index 438bdbc7f6..46fdac5f6b 100644 --- a/packages/parser/src/types/parser.ts +++ b/packages/parser/src/types/parser.ts @@ -1,4 +1,5 @@ import type { StandardSchemaV1 } from '@standard-schema/spec'; +import type { ParseError } from '../errors.js'; import type { ArrayEnvelope, DynamoDBArrayEnvelope, @@ -17,6 +18,7 @@ type ParserOptions< schema: TSchema; envelope?: TEnvelope; safeParse?: TSafeParse; + errorHandler?: (error: ParseError) => unknown; }; /** diff --git a/packages/parser/tests/unit/parser.decorator.test.ts b/packages/parser/tests/unit/parser.decorator.test.ts index 067755b9e4..a69215ce16 100644 --- a/packages/parser/tests/unit/parser.decorator.test.ts +++ b/packages/parser/tests/unit/parser.decorator.test.ts @@ -64,6 +64,28 @@ describe('Decorator: parser', () => { return event; } + @parser({ + schema, + errorHandler: (_error) => undefined, + }) + public async handlerWithErrorHandlerReturningUndefined( + event: z.infer, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema, + errorHandler: (error) => ({ errorHandled: true, message: error.message }), + }) + public async handlerWithErrorHandler( + event: z.infer, + _context: Context + ): Promise { + return event; + } + private anotherMethod(event: unknown): unknown { return event; } @@ -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, + {} 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, + {} 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, + {} 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); + }); }); diff --git a/packages/parser/tests/unit/parser.middy.test.ts b/packages/parser/tests/unit/parser.middy.test.ts index c138a3e5cf..ae6b8ffccb 100644 --- a/packages/parser/tests/unit/parser.middy.test.ts +++ b/packages/parser/tests/unit/parser.middy.test.ts @@ -190,4 +190,75 @@ describe('Middleware: parser', () => { 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) + ).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); + + // 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, + {} 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) + ).rejects.toThrow(ParseError); + }); });