Skip to content

Commit f8100e8

Browse files
nateilerdreamorosi
andauthored
fix(event-handler): default error handler returns a web Response correctly (#5024)
Co-authored-by: Andrea Amorosi <[email protected]>
1 parent 79553c9 commit f8100e8

3 files changed

Lines changed: 139 additions & 27 deletions

File tree

packages/event-handler/src/http/Router.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,10 +489,7 @@ class Router {
489489
}
490490

491491
if (error instanceof HttpError) {
492-
return new Response(JSON.stringify(error.toJSON()), {
493-
status: error.statusCode,
494-
headers: { 'Content-Type': 'application/json' },
495-
});
492+
return error.toWebResponse();
496493
}
497494

498495
return this.#defaultErrorHandler(error);

packages/event-handler/src/http/errors.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
2-
import type { HandlerResponse, HttpStatusCode } from '../types/http.js';
2+
import type { HttpStatusCode } from '../types/http.js';
33
import { HttpStatusCodes } from './constants.js';
44

55
class RouteMatchingError extends Error {
@@ -35,7 +35,7 @@ abstract class HttpError extends Error {
3535
this.details = details;
3636
}
3737

38-
toJSON(): HandlerResponse {
38+
toJSON(): JSONValue {
3939
return {
4040
statusCode: this.statusCode,
4141
error: this.errorType,
@@ -45,6 +45,12 @@ abstract class HttpError extends Error {
4545
}),
4646
};
4747
}
48+
49+
toWebResponse(): Response {
50+
return Response.json(this.toJSON(), {
51+
status: this.statusCode,
52+
});
53+
}
4854
}
4955

5056
class BadRequestError extends HttpError {

packages/event-handler/tests/unit/http/errors.test.ts

Lines changed: 130 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,17 @@ describe('HTTP Error Classes', () => {
6868
statusCode: HttpStatusCodes.SERVICE_UNAVAILABLE,
6969
customMessage: 'Maintenance mode',
7070
},
71-
])(
72-
'$errorType uses custom message when provided',
73-
({ ErrorClass, errorType, statusCode, customMessage }) => {
74-
const error = new ErrorClass(customMessage);
75-
expect(error.message).toBe(customMessage);
76-
expect(error.statusCode).toBe(statusCode);
77-
expect(error.errorType).toBe(errorType);
78-
}
79-
);
71+
])('$errorType uses custom message when provided', ({
72+
ErrorClass,
73+
errorType,
74+
statusCode,
75+
customMessage,
76+
}) => {
77+
const error = new ErrorClass(customMessage);
78+
expect(error.message).toBe(customMessage);
79+
expect(error.statusCode).toBe(statusCode);
80+
expect(error.errorType).toBe(errorType);
81+
});
8082

8183
describe('toJSON', () => {
8284
it.each([
@@ -134,19 +136,21 @@ describe('HTTP Error Classes', () => {
134136
statusCode: HttpStatusCodes.SERVICE_UNAVAILABLE,
135137
message: 'Maintenance mode',
136138
},
137-
])(
138-
'$errorType serializes to JSON format',
139-
({ ErrorClass, errorType, statusCode, message }) => {
140-
const error = new ErrorClass(message);
141-
const json = error.toJSON();
139+
])('$errorType serializes to JSON format', ({
140+
ErrorClass,
141+
errorType,
142+
statusCode,
143+
message,
144+
}) => {
145+
const error = new ErrorClass(message);
146+
const json = error.toJSON();
142147

143-
expect(json).toEqual({
144-
statusCode,
145-
error: errorType,
146-
message,
147-
});
148-
}
149-
);
148+
expect(json).toEqual({
149+
statusCode,
150+
error: errorType,
151+
message,
152+
});
153+
});
150154

151155
it('includes details in JSON when provided', () => {
152156
const details = { field: 'value', code: 'VALIDATION_ERROR' };
@@ -174,6 +178,111 @@ describe('HTTP Error Classes', () => {
174178
});
175179
});
176180

181+
describe('toWebResponse', () => {
182+
it.each([
183+
{
184+
ErrorClass: BadRequestError,
185+
errorType: 'BadRequestError',
186+
statusCode: HttpStatusCodes.BAD_REQUEST,
187+
message: 'Invalid input',
188+
},
189+
{
190+
ErrorClass: UnauthorizedError,
191+
errorType: 'UnauthorizedError',
192+
statusCode: HttpStatusCodes.UNAUTHORIZED,
193+
message: 'Token expired',
194+
},
195+
{
196+
ErrorClass: ForbiddenError,
197+
errorType: 'ForbiddenError',
198+
statusCode: HttpStatusCodes.FORBIDDEN,
199+
message: 'Access denied',
200+
},
201+
{
202+
ErrorClass: NotFoundError,
203+
errorType: 'NotFoundError',
204+
statusCode: HttpStatusCodes.NOT_FOUND,
205+
message: 'Resource not found',
206+
},
207+
{
208+
ErrorClass: MethodNotAllowedError,
209+
errorType: 'MethodNotAllowedError',
210+
statusCode: HttpStatusCodes.METHOD_NOT_ALLOWED,
211+
message: 'POST not allowed',
212+
},
213+
{
214+
ErrorClass: RequestTimeoutError,
215+
errorType: 'RequestTimeoutError',
216+
statusCode: HttpStatusCodes.REQUEST_TIMEOUT,
217+
message: 'Operation timed out',
218+
},
219+
{
220+
ErrorClass: RequestEntityTooLargeError,
221+
errorType: 'RequestEntityTooLargeError',
222+
statusCode: HttpStatusCodes.REQUEST_ENTITY_TOO_LARGE,
223+
message: 'File too large',
224+
},
225+
{
226+
ErrorClass: InternalServerError,
227+
errorType: 'InternalServerError',
228+
statusCode: HttpStatusCodes.INTERNAL_SERVER_ERROR,
229+
message: 'Database connection failed',
230+
},
231+
{
232+
ErrorClass: ServiceUnavailableError,
233+
errorType: 'ServiceUnavailableError',
234+
statusCode: HttpStatusCodes.SERVICE_UNAVAILABLE,
235+
message: 'Maintenance mode',
236+
},
237+
])('$errorType creates Response object', async ({
238+
ErrorClass,
239+
errorType,
240+
statusCode,
241+
message,
242+
}) => {
243+
const error = new ErrorClass(message);
244+
const response = error.toWebResponse();
245+
246+
expect(response.status).toEqual(statusCode);
247+
expect(response.headers.get('Content-Type')).toEqual('application/json');
248+
249+
await expect(response.json()).resolves.toEqual({
250+
statusCode,
251+
error: errorType,
252+
message,
253+
});
254+
});
255+
256+
it('includes details in Response body when provided', async () => {
257+
const details = { field: 'value', code: 'VALIDATION_ERROR' };
258+
const error = new BadRequestError('Invalid input', undefined, details);
259+
const response = error.toWebResponse();
260+
261+
expect(response.status).toEqual(HttpStatusCodes.BAD_REQUEST);
262+
263+
await expect(response.json()).resolves.toEqual({
264+
statusCode: HttpStatusCodes.BAD_REQUEST,
265+
error: 'BadRequestError',
266+
message: 'Invalid input',
267+
details,
268+
});
269+
});
270+
271+
it('excludes details from JSON when not provided', async () => {
272+
const error = new BadRequestError('Invalid input');
273+
const response = error.toWebResponse();
274+
275+
expect(response.status).toEqual(HttpStatusCodes.BAD_REQUEST);
276+
277+
await expect(response.json()).resolves.toEqual({
278+
statusCode: HttpStatusCodes.BAD_REQUEST,
279+
error: 'BadRequestError',
280+
message: 'Invalid input',
281+
});
282+
expect(response).not.toHaveProperty('details');
283+
});
284+
});
285+
177286
it('passes options to Error superclass', () => {
178287
const cause = new Error('Root cause');
179288
const error = new BadRequestError('Invalid input', { cause });

0 commit comments

Comments
 (0)