Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
16 changes: 16 additions & 0 deletions packages/event-handler/src/rest/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,27 @@ class Router {
try {
const { scope, ...reqCtx } = options;
const body = await handler.apply(scope ?? this, [error, reqCtx]);
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' },
});
} 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);
}
}
Expand Down
32 changes: 23 additions & 9 deletions packages/event-handler/src/types/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ 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;
};
type ErrorResponse =
| {
statusCode: HttpStatusCode;
error: string;
message?: string;
}
| {
statusCode: HttpStatusCode;
error?: string;
message: string;
};

type RequestContext = {
req: Request;
Expand All @@ -24,10 +34,14 @@ type RequestContext = {

type ErrorResolveOptions = RequestContext & ResolveOptions;

type ErrorHandler<T extends Error = Error> = (
error: T,
reqCtx: RequestContext
) => Promise<ErrorResponse>;
type ErrorHandler<T extends Error = Error> = T extends
| NotFoundError
| MethodNotAllowedError
? (
error: T,
reqCtx: RequestContext
) => Promise<Omit<ErrorResponse, 'statusCode'> | Response>
: (error: T, reqCtx: RequestContext) => Promise<ErrorResponse | Response>;

interface ErrorConstructor<T extends Error = Error> {
new (...args: any[]): T;
Expand Down
103 changes: 99 additions & 4 deletions packages/event-handler/tests/unit/rest/Router/error-handling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
HttpErrorCodes,
InternalServerError,
MethodNotAllowedError,
NotFoundError,
Router,
} from '../../../../src/rest/index.js';
import { createTestEvent } from '../helpers.js';
Expand Down Expand Up @@ -49,7 +50,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}`,
}));
Expand All @@ -64,9 +64,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,
Expand All @@ -78,7 +78,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}`,
}));
Expand All @@ -94,9 +93,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,
Expand Down Expand Up @@ -393,4 +392,100 @@ 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,
});
});

it('handles throwing a built in NotFound 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,
});
});

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,
});
});
});
Loading