From e25a22ada6bedf5fb580a6c61de6273426187888 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 10:09:17 +0100 Subject: [PATCH 01/10] Supported returning response directly on error handler --- packages/event-handler/src/rest/Router.ts | 3 ++ packages/event-handler/src/types/rest.ts | 2 +- .../unit/rest/Router/error-handling.test.ts | 38 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 313fb88ffd..5270638dca 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -316,6 +316,9 @@ class Router { try { const { scope, ...reqCtx } = options; const body = await handler.apply(scope ?? this, [error, reqCtx]); + if (body instanceof Response) { + return body; + } return new Response(JSON.stringify(body), { status: body.statusCode, headers: { 'Content-Type': 'application/json' }, diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index 456a1dec9d..545fe5d1d3 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -27,7 +27,7 @@ type ErrorResolveOptions = RequestContext & ResolveOptions; type ErrorHandler = ( error: T, reqCtx: RequestContext -) => Promise; +) => Promise; interface ErrorConstructor { new (...args: any[]): T; diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index d18778bcb0..38714177b5 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -393,4 +393,42 @@ describe('Class: Router - Error Handling', () => { expect(body.hasEvent).toBe(true); expect(body.hasContext).toBe(true); }); + + it('handles returning a Response from the error handler', async () => { + // Prepare + const app = new Router(); + + app.errorHandler( + BadRequestError, + async () => + new Response( + JSON.stringify({ + foo: 'bar', + }), + { + status: HttpErrorCodes.BAD_REQUEST, + headers: { + 'content-type': 'application/json', + }, + } + ) + ); + + app.get('/test', () => { + throw new BadRequestError('test error'); + }); + + // Act + const result = await app.resolve(createTestEvent('/test', 'GET'), context); + + // Assess + expect(result).toEqual({ + statusCode: HttpErrorCodes.BAD_REQUEST, + body: JSON.stringify({ + foo: 'bar', + }), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + }); }); From f9397952c0c2676732b288921141510bf398c5e1 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 11:37:28 +0100 Subject: [PATCH 02/10] omitted the statusCode when using NotFoundError or MethodNotAllowedError --- packages/event-handler/src/rest/Router.ts | 7 +++++++ packages/event-handler/src/types/rest.ts | 16 ++++++++++++---- .../unit/rest/Router/error-handling.test.ts | 6 ++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 5270638dca..1bc14fe975 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -319,6 +319,13 @@ class Router { if (body instanceof Response) { return body; } + if (!body.statusCode) { + if (error instanceof NotFoundError) { + body.statusCode = HttpErrorCodes.NOT_FOUND; + } else if (error instanceof MethodNotAllowedError) { + body.statusCode = HttpErrorCodes.METHOD_NOT_ALLOWED; + } + } return new Response(JSON.stringify(body), { status: body.statusCode, headers: { 'Content-Type': 'application/json' }, diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index 545fe5d1d3..045c810cff 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -3,6 +3,10 @@ import type { JSONObject, } from '@aws-lambda-powertools/commons/types'; import type { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import type { + MethodNotAllowedError, + NotFoundError, +} from '../../src/rest/errors.js'; import type { HttpErrorCodes, HttpVerbs } from '../rest/constants.js'; import type { Route } from '../rest/Route.js'; import type { Router } from '../rest/Router.js'; @@ -24,10 +28,14 @@ type RequestContext = { type ErrorResolveOptions = RequestContext & ResolveOptions; -type ErrorHandler = ( - error: T, - reqCtx: RequestContext -) => Promise; +type ErrorHandler = T extends + | NotFoundError + | MethodNotAllowedError + ? ( + error: T, + reqCtx: RequestContext + ) => Promise | Response> + : (error: T, reqCtx: RequestContext) => Promise; interface ErrorConstructor { new (...args: any[]): T; diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 38714177b5..6f859f9a6d 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -49,7 +49,6 @@ describe('Class: Router - Error Handling', () => { const app = new Router(); app.notFound(async (error) => ({ - statusCode: HttpErrorCodes.NOT_FOUND, error: 'Not Found', message: `Custom: ${error.message}`, })); @@ -64,9 +63,9 @@ describe('Class: Router - Error Handling', () => { expect(result).toEqual({ statusCode: HttpErrorCodes.NOT_FOUND, body: JSON.stringify({ - statusCode: HttpErrorCodes.NOT_FOUND, error: 'Not Found', message: 'Custom: Route /nonexistent for method GET not found', + statusCode: HttpErrorCodes.NOT_FOUND, }), headers: { 'content-type': 'application/json' }, isBase64Encoded: false, @@ -78,7 +77,6 @@ describe('Class: Router - Error Handling', () => { const app = new Router(); app.methodNotAllowed(async (error) => ({ - statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, error: 'Method Not Allowed', message: `Custom: ${error.message}`, })); @@ -94,9 +92,9 @@ describe('Class: Router - Error Handling', () => { expect(result).toEqual({ statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, body: JSON.stringify({ - statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, error: 'Method Not Allowed', message: 'Custom: POST not allowed', + statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, }), headers: { 'content-type': 'application/json' }, isBase64Encoded: false, From 246510f33e3b874c8ceb80f144785e24e14939ae Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 12:47:01 +0100 Subject: [PATCH 03/10] allowed throwing of built in errors from the error handler --- packages/event-handler/src/rest/Router.ts | 6 ++++ packages/event-handler/src/types/rest.ts | 16 ++++++---- .../unit/rest/Router/error-handling.test.ts | 29 +++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 1bc14fe975..44c5966787 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -331,6 +331,12 @@ class Router { headers: { 'Content-Type': 'application/json' }, }); } catch (handlerError) { + if (handlerError instanceof NotFoundError) { + return await this.handleError(handlerError, options); + } + if (handlerError instanceof MethodNotAllowedError) { + return await this.handleError(handlerError, options); + } return this.#defaultErrorHandler(handlerError as Error); } } diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index 045c810cff..2a206b5d68 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -12,11 +12,17 @@ import type { Route } from '../rest/Route.js'; import type { Router } from '../rest/Router.js'; import type { ResolveOptions } from './common.js'; -type ErrorResponse = { - statusCode: HttpStatusCode; - error: string; - message: string; -}; +type ErrorResponse = + | { + statusCode: HttpStatusCode; + error: string; + message?: string; + } + | { + statusCode: HttpStatusCode; + error?: string; + message: string; + }; type RequestContext = { req: Request; diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 6f859f9a6d..bcdfe83cb1 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -5,6 +5,7 @@ import { HttpErrorCodes, InternalServerError, MethodNotAllowedError, + NotFoundError, Router, } from '../../../../src/rest/index.js'; import { createTestEvent } from '../helpers.js'; @@ -429,4 +430,32 @@ describe('Class: Router - Error Handling', () => { isBase64Encoded: false, }); }); + + it('handles throwing a built in error from the error handler', async () => { + // Prepare + const app = new Router(); + + app.errorHandler(BadRequestError, async () => { + throw new NotFoundError('This error is thrown from the error handler'); + }); + + app.get('/test', () => { + throw new BadRequestError('test error'); + }); + + // Act + const result = await app.resolve(createTestEvent('/test', 'GET'), context); + + // Assess + expect(result).toEqual({ + statusCode: HttpErrorCodes.NOT_FOUND, + body: JSON.stringify({ + statusCode: HttpErrorCodes.NOT_FOUND, + error: 'NotFoundError', + message: 'This error is thrown from the error handler', + }), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + }); }); From 3102fefafedce70b0abf8b49bad55719132f135f Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 12:52:39 +0100 Subject: [PATCH 04/10] improved coverage --- .../unit/rest/Router/error-handling.test.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index bcdfe83cb1..2cbe8d1df8 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -431,7 +431,7 @@ describe('Class: Router - Error Handling', () => { }); }); - it('handles throwing a built in error from the error handler', async () => { + it('handles throwing a built in NotFound error from the error handler', async () => { // Prepare const app = new Router(); @@ -458,4 +458,34 @@ describe('Class: Router - Error Handling', () => { isBase64Encoded: false, }); }); + + it('handles throwing a built in MethodNotAllowedError error from the error handler', async () => { + // Prepare + const app = new Router(); + + app.errorHandler(BadRequestError, async () => { + throw new MethodNotAllowedError( + 'This error is thrown from the error handler' + ); + }); + + app.get('/test', () => { + throw new BadRequestError('test error'); + }); + + // Act + const result = await app.resolve(createTestEvent('/test', 'GET'), context); + + // Assess + expect(result).toEqual({ + statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, + body: JSON.stringify({ + statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, + error: 'MethodNotAllowedError', + message: 'This error is thrown from the error handler', + }), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + }); }); From ea448b11e184d23af0f1ffba939a629f301e15fc Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 13:35:33 +0100 Subject: [PATCH 05/10] allowed returning JSONObject from error handler --- packages/event-handler/src/rest/Router.ts | 7 ++---- packages/event-handler/src/types/rest.ts | 7 ++++-- .../unit/rest/Router/error-handling.test.ts | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 44c5966787..6fb07e6837 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -327,14 +327,11 @@ class Router { } } return new Response(JSON.stringify(body), { - status: body.statusCode, + status: (body.statusCode as number) ?? HttpErrorCodes.OK, headers: { 'Content-Type': 'application/json' }, }); } catch (handlerError) { - if (handlerError instanceof NotFoundError) { - return await this.handleError(handlerError, options); - } - if (handlerError instanceof MethodNotAllowedError) { + if (handlerError instanceof ServiceError) { return await this.handleError(handlerError, options); } return this.#defaultErrorHandler(handlerError as Error); diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index 2a206b5d68..fc6710014e 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -40,8 +40,11 @@ type ErrorHandler = T extends ? ( error: T, reqCtx: RequestContext - ) => Promise | Response> - : (error: T, reqCtx: RequestContext) => Promise; + ) => Promise | Response | JSONObject> + : ( + error: T, + reqCtx: RequestContext + ) => Promise; interface ErrorConstructor { new (...args: any[]): T; diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 2cbe8d1df8..0c5d312011 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -431,6 +431,30 @@ describe('Class: Router - Error Handling', () => { }); }); + it('handles returning a JSONObject from the error handler', async () => { + // Prepare + const app = new Router(); + + app.errorHandler(BadRequestError, async () => ({ foo: 'bar' })); + + app.get('/test', () => { + throw new BadRequestError('test error'); + }); + + // Act + const result = await app.resolve(createTestEvent('/test', 'GET'), context); + + // Assess + expect(result).toEqual({ + statusCode: HttpErrorCodes.OK, + body: JSON.stringify({ + foo: 'bar', + }), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + }); + it('handles throwing a built in NotFound error from the error handler', async () => { // Prepare const app = new Router(); From e6b5cf213ae36f474788bf054ffd0be7d8b601a6 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 14:08:47 +0100 Subject: [PATCH 06/10] changed fallback error code to 500 --- packages/event-handler/src/rest/Router.ts | 3 ++- .../tests/unit/rest/Router/error-handling.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 6fb07e6837..542c756ab4 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -327,7 +327,8 @@ class Router { } } return new Response(JSON.stringify(body), { - status: (body.statusCode as number) ?? HttpErrorCodes.OK, + status: + (body.statusCode as number) ?? HttpErrorCodes.INTERNAL_SERVER_ERROR, headers: { 'Content-Type': 'application/json' }, }); } catch (handlerError) { diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 0c5d312011..2b6dc12e86 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -446,7 +446,7 @@ describe('Class: Router - Error Handling', () => { // Assess expect(result).toEqual({ - statusCode: HttpErrorCodes.OK, + statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, body: JSON.stringify({ foo: 'bar', }), From 696d10f93ed6e0bf6b504dad93684eafe3f91243 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 16:27:40 +0100 Subject: [PATCH 07/10] removed the ErrorResponse and used HandlerResponse --- packages/event-handler/src/rest/Router.ts | 15 ++++++--- packages/event-handler/src/rest/converters.ts | 8 +++-- packages/event-handler/src/rest/errors.ts | 4 +-- packages/event-handler/src/types/index.ts | 1 - packages/event-handler/src/types/rest.ts | 32 +++---------------- 5 files changed, 21 insertions(+), 39 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 542c756ab4..d04509c35b 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -4,7 +4,7 @@ import { isDevMode, } from '@aws-lambda-powertools/commons/utils/env'; import type { APIGatewayProxyResult, Context } from 'aws-lambda'; -import type { ResolveOptions } from '../types/index.js'; +import type { HandlerResponse, ResolveOptions } from '../types/index.js'; import type { ErrorConstructor, ErrorHandler, @@ -22,7 +22,6 @@ import { handlerResultToProxyResult, handlerResultToWebResponse, proxyEventToWebRequest, - webResponseToProxyResult, } from './converters.js'; import { ErrorHandlerRegistry } from './ErrorHandlerRegistry.js'; import { @@ -36,6 +35,7 @@ import { RouteHandlerRegistry } from './RouteHandlerRegistry.js'; import { composeMiddleware, isAPIGatewayProxyEvent, + isAPIGatewayProxyResult, isHttpMethod, } from './utils.js'; @@ -280,7 +280,12 @@ class Router { ...requestContext, scope: options?.scope, }); - return await webResponseToProxyResult(result); + return handlerResultToProxyResult( + result, + (result.status ?? + result.statusCode ?? + HttpErrorCodes.INTERNAL_SERVER_ERROR) as (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes] + ); } } @@ -310,13 +315,13 @@ class Router { protected async handleError( error: Error, options: ErrorResolveOptions - ): Promise { + ): Promise { const handler = this.errorHandlerRegistry.resolve(error); if (handler !== null) { try { const { scope, ...reqCtx } = options; const body = await handler.apply(scope ?? this, [error, reqCtx]); - if (body instanceof Response) { + if (body instanceof Response || isAPIGatewayProxyResult(body)) { return body; } if (!body.statusCode) { diff --git a/packages/event-handler/src/rest/converters.ts b/packages/event-handler/src/rest/converters.ts index d82f2ada5c..91180ced4f 100644 --- a/packages/event-handler/src/rest/converters.ts +++ b/packages/event-handler/src/rest/converters.ts @@ -1,6 +1,6 @@ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import type { CompressionOptions, HandlerResponse } from '../types/rest.js'; -import { COMPRESSION_ENCODING_TYPES } from './constants.js'; +import { COMPRESSION_ENCODING_TYPES, HttpErrorCodes } from './constants.js'; import { isAPIGatewayProxyResult } from './utils.js'; /** @@ -181,10 +181,12 @@ export const handlerResultToWebResponse = ( * Handles APIGatewayProxyResult, Response objects, and plain objects. * * @param response - The handler response (APIGatewayProxyResult, Response, or plain object) + * @param statusCode - The response status code to return * @returns An API Gateway proxy result */ export const handlerResultToProxyResult = async ( - response: HandlerResponse + response: HandlerResponse, + statusCode: (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes] = HttpErrorCodes.OK ): Promise => { if (isAPIGatewayProxyResult(response)) { return response; @@ -193,7 +195,7 @@ export const handlerResultToProxyResult = async ( return await webResponseToProxyResult(response); } return { - statusCode: 200, + statusCode, body: JSON.stringify(response), headers: { 'content-type': 'application/json' }, isBase64Encoded: false, diff --git a/packages/event-handler/src/rest/errors.ts b/packages/event-handler/src/rest/errors.ts index f03421bc9c..2f3d350f3d 100644 --- a/packages/event-handler/src/rest/errors.ts +++ b/packages/event-handler/src/rest/errors.ts @@ -1,4 +1,4 @@ -import type { ErrorResponse, HttpStatusCode } from '../types/rest.js'; +import type { HandlerResponse, HttpStatusCode } from '../types/rest.js'; import { HttpErrorCodes } from './constants.js'; export class RouteMatchingError extends Error { @@ -34,7 +34,7 @@ export abstract class ServiceError extends Error { this.details = details; } - toJSON(): ErrorResponse { + toJSON(): HandlerResponse { return { statusCode: this.statusCode, error: this.errorType, diff --git a/packages/event-handler/src/types/index.ts b/packages/event-handler/src/types/index.ts index 56e017c563..b32b78c70f 100644 --- a/packages/event-handler/src/types/index.ts +++ b/packages/event-handler/src/types/index.ts @@ -35,7 +35,6 @@ export type { CorsOptions, ErrorHandler, ErrorResolveOptions, - ErrorResponse, HandlerResponse, HttpMethod, HttpStatusCode, diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index fc6710014e..188d3ff29a 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -3,27 +3,11 @@ import type { JSONObject, } from '@aws-lambda-powertools/commons/types'; import type { APIGatewayProxyEvent, Context } from 'aws-lambda'; -import type { - MethodNotAllowedError, - NotFoundError, -} from '../../src/rest/errors.js'; import type { HttpErrorCodes, HttpVerbs } from '../rest/constants.js'; import type { Route } from '../rest/Route.js'; import type { Router } from '../rest/Router.js'; import type { ResolveOptions } from './common.js'; -type ErrorResponse = - | { - statusCode: HttpStatusCode; - error: string; - message?: string; - } - | { - statusCode: HttpStatusCode; - error?: string; - message: string; - }; - type RequestContext = { req: Request; event: APIGatewayProxyEvent; @@ -34,17 +18,10 @@ type RequestContext = { type ErrorResolveOptions = RequestContext & ResolveOptions; -type ErrorHandler = T extends - | NotFoundError - | MethodNotAllowedError - ? ( - error: T, - reqCtx: RequestContext - ) => Promise | Response | JSONObject> - : ( - error: T, - reqCtx: RequestContext - ) => Promise; +type ErrorHandler = ( + error: T, + reqCtx: RequestContext +) => Promise; interface ErrorConstructor { new (...args: any[]): T; @@ -182,7 +159,6 @@ export type { CompiledRoute, CorsOptions, DynamicRoute, - ErrorResponse, ErrorConstructor, ErrorHandlerRegistryOptions, ErrorHandler, From aa9dbe949cb46f5ad328a862917ed9e9b54be092 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 18:01:51 +0100 Subject: [PATCH 08/10] fixed the type issues and coverage --- packages/event-handler/src/rest/Router.ts | 6 ++--- packages/event-handler/src/rest/errors.ts | 5 +++- .../unit/rest/Router/error-handling.test.ts | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index d04509c35b..e278c6ef73 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -280,11 +280,11 @@ class Router { ...requestContext, scope: options?.scope, }); + const statusCode = + result instanceof Response ? result.status : result.statusCode; return handlerResultToProxyResult( result, - (result.status ?? - result.statusCode ?? - HttpErrorCodes.INTERNAL_SERVER_ERROR) as (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes] + statusCode as (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes] ); } } diff --git a/packages/event-handler/src/rest/errors.ts b/packages/event-handler/src/rest/errors.ts index 2f3d350f3d..80fa0cbc2a 100644 --- a/packages/event-handler/src/rest/errors.ts +++ b/packages/event-handler/src/rest/errors.ts @@ -1,3 +1,4 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import type { HandlerResponse, HttpStatusCode } from '../types/rest.js'; import { HttpErrorCodes } from './constants.js'; @@ -39,7 +40,9 @@ export abstract class ServiceError extends Error { statusCode: this.statusCode, error: this.errorType, message: this.message, - ...(this.details && { details: this.details }), + ...(this.details && { + details: this.details as Record, + }), }; } } diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 2b6dc12e86..9caf0d3338 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -431,6 +431,33 @@ describe('Class: Router - Error Handling', () => { }); }); + it('handles returning an API Gateway Proxy result from the error handler', async () => { + // Prepare + const app = new Router(); + + app.errorHandler(BadRequestError, async () => ({ + statusCode: HttpErrorCodes.BAD_REQUEST, + body: JSON.stringify({ + foo: 'bar', + }), + })); + + app.get('/test', () => { + throw new BadRequestError('test error'); + }); + + // Act + const result = await app.resolve(createTestEvent('/test', 'GET'), context); + + // Assess + expect(result).toEqual({ + statusCode: HttpErrorCodes.BAD_REQUEST, + body: JSON.stringify({ + foo: 'bar', + }), + }); + }); + it('handles returning a JSONObject from the error handler', async () => { // Prepare const app = new Router(); From 5ee4241a439b59f0ced490bafa1c8df55804ad28 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 18:45:36 +0100 Subject: [PATCH 09/10] Changed the HttpStatusCode type and added test to throw a generic error --- packages/event-handler/src/rest/Router.ts | 11 ++++---- packages/event-handler/src/rest/converters.ts | 8 ++++-- .../unit/rest/Router/error-handling.test.ts | 28 +++++++++++++++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index e278c6ef73..8d1766a2dc 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -4,7 +4,11 @@ import { isDevMode, } from '@aws-lambda-powertools/commons/utils/env'; import type { APIGatewayProxyResult, Context } from 'aws-lambda'; -import type { HandlerResponse, ResolveOptions } from '../types/index.js'; +import type { + HandlerResponse, + HttpStatusCode, + ResolveOptions, +} from '../types/index.js'; import type { ErrorConstructor, ErrorHandler, @@ -282,10 +286,7 @@ class Router { }); const statusCode = result instanceof Response ? result.status : result.statusCode; - return handlerResultToProxyResult( - result, - statusCode as (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes] - ); + return handlerResultToProxyResult(result, statusCode as HttpStatusCode); } } diff --git a/packages/event-handler/src/rest/converters.ts b/packages/event-handler/src/rest/converters.ts index 91180ced4f..30948b3d9b 100644 --- a/packages/event-handler/src/rest/converters.ts +++ b/packages/event-handler/src/rest/converters.ts @@ -1,5 +1,9 @@ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; -import type { CompressionOptions, HandlerResponse } from '../types/rest.js'; +import type { + CompressionOptions, + HandlerResponse, + HttpStatusCode, +} from '../types/rest.js'; import { COMPRESSION_ENCODING_TYPES, HttpErrorCodes } from './constants.js'; import { isAPIGatewayProxyResult } from './utils.js'; @@ -186,7 +190,7 @@ export const handlerResultToWebResponse = ( */ export const handlerResultToProxyResult = async ( response: HandlerResponse, - statusCode: (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes] = HttpErrorCodes.OK + statusCode: HttpStatusCode = HttpErrorCodes.OK ): Promise => { if (isAPIGatewayProxyResult(response)) { return response; diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 9caf0d3338..52c0cd450c 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -539,4 +539,32 @@ describe('Class: Router - Error Handling', () => { isBase64Encoded: false, }); }); + + it('handles throwing a generic error from the error handler', async () => { + // Prepare + const app = new Router(); + + app.errorHandler(BadRequestError, async () => { + throw new Error('This error is thrown from the error handler'); + }); + + app.get('/test', () => { + throw new BadRequestError('test error'); + }); + + // Act + const result = await app.resolve(createTestEvent('/test', 'GET'), context); + + // Assess + expect(result).toEqual({ + statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, + body: JSON.stringify({ + statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, + error: 'Internal Server Error', + message: 'Internal Server Error', + }), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + }); }); From c837301b2c138b3b8218c02cf8a76b66228d7596 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Tue, 23 Sep 2025 20:18:22 +0100 Subject: [PATCH 10/10] updated test to check the details of the error --- .../unit/rest/Router/error-handling.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 52c0cd450c..9795117dc1 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -542,6 +542,7 @@ describe('Class: Router - Error Handling', () => { it('handles throwing a generic error from the error handler', async () => { // Prepare + vi.stubEnv('POWERTOOLS_DEV', 'true'); const app = new Router(); app.errorHandler(BadRequestError, async () => { @@ -556,15 +557,13 @@ describe('Class: Router - Error Handling', () => { const result = await app.resolve(createTestEvent('/test', 'GET'), context); // Assess - expect(result).toEqual({ - statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, - body: JSON.stringify({ - statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, - error: 'Internal Server Error', - message: 'Internal Server Error', - }), - headers: { 'content-type': 'application/json' }, - isBase64Encoded: false, + expect(result.statusCode).toBe(HttpErrorCodes.INTERNAL_SERVER_ERROR); + const body = JSON.parse(result.body); + expect(body.error).toBe('Internal Server Error'); + expect(body.message).toBe('This error is thrown from the error handler'); + expect(body.stack).toBeDefined(); + expect(body.details).toEqual({ + errorName: 'Error', }); }); });