Skip to content

feat(event-handler): add error classes for http errors #4299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 11, 2025
Merged
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
74 changes: 74 additions & 0 deletions packages/event-handler/src/rest/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,80 @@ export const HttpVerbs = {
OPTIONS: 'OPTIONS',
} as const;

export const HttpErrorCodes = {
// 1xx Informational
CONTINUE: 100,
SWITCHING_PROTOCOLS: 101,
PROCESSING: 102,
EARLY_HINTS: 103,

// 2xx Success
OK: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
RESET_CONTENT: 205,
PARTIAL_CONTENT: 206,
MULTI_STATUS: 207,
ALREADY_REPORTED: 208,
IM_USED: 226,

// 3xx Redirection
MULTIPLE_CHOICES: 300,
MOVED_PERMANENTLY: 301,
FOUND: 302,
SEE_OTHER: 303,
NOT_MODIFIED: 304,
USE_PROXY: 305,
TEMPORARY_REDIRECT: 307,
PERMANENT_REDIRECT: 308,

// 4xx Client Error
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
NOT_ACCEPTABLE: 406,
PROXY_AUTHENTICATION_REQUIRED: 407,
REQUEST_TIMEOUT: 408,
CONFLICT: 409,
GONE: 410,
LENGTH_REQUIRED: 411,
PRECONDITION_FAILED: 412,
REQUEST_ENTITY_TOO_LARGE: 413,
REQUEST_URI_TOO_LONG: 414,
UNSUPPORTED_MEDIA_TYPE: 415,
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
EXPECTATION_FAILED: 417,
IM_A_TEAPOT: 418,
MISDIRECTED_REQUEST: 421,
UNPROCESSABLE_ENTITY: 422,
LOCKED: 423,
FAILED_DEPENDENCY: 424,
TOO_EARLY: 425,
UPGRADE_REQUIRED: 426,
PRECONDITION_REQUIRED: 428,
TOO_MANY_REQUESTS: 429,
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
UNAVAILABLE_FOR_LEGAL_REASONS: 451,

// 5xx Server Error
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
HTTP_VERSION_NOT_SUPPORTED: 505,
VARIANT_ALSO_NEGOTIATES: 506,
INSUFFICIENT_STORAGE: 507,
LOOP_DETECTED: 508,
NOT_EXTENDED: 510,
NETWORK_AUTHENTICATION_REQUIRED: 511,
} as const;

export const PARAM_PATTERN = /:([a-zA-Z_]\w*)(?=\/|$)/g;

export const SAFE_CHARS = "-._~()'!*:@,;=+&$";
Expand Down
145 changes: 145 additions & 0 deletions packages/event-handler/src/rest/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { ErrorResponse, HttpStatusCode } from '../types/rest.js';
import { HttpErrorCodes } from './constants.js';

export class RouteMatchingError extends Error {
constructor(
message: string,
Expand All @@ -15,3 +18,145 @@ export class ParameterValidationError extends RouteMatchingError {
this.name = 'ParameterValidationError';
}
}

abstract class ServiceError extends Error {
abstract readonly statusCode: HttpStatusCode;
abstract readonly errorType: string;
public readonly details?: Record<string, unknown>;

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options);
this.name = 'ServiceError';
this.details = details;
}

toJSON(): ErrorResponse {
return {
statusCode: this.statusCode,
error: this.errorType,
message: this.message,
...(this.details && { details: this.details }),
};
}
}

export class BadRequestError extends ServiceError {
readonly statusCode = HttpErrorCodes.BAD_REQUEST;
readonly errorType = 'BadRequestError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class UnauthorizedError extends ServiceError {
readonly statusCode = HttpErrorCodes.UNAUTHORIZED;
readonly errorType = 'UnauthorizedError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class ForbiddenError extends ServiceError {
readonly statusCode = HttpErrorCodes.FORBIDDEN;
readonly errorType = 'ForbiddenError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class NotFoundError extends ServiceError {
readonly statusCode = HttpErrorCodes.NOT_FOUND;
readonly errorType = 'NotFoundError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class MethodNotAllowedError extends ServiceError {
readonly statusCode = HttpErrorCodes.METHOD_NOT_ALLOWED;
readonly errorType = 'MethodNotAllowedError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class RequestTimeoutError extends ServiceError {
readonly statusCode = HttpErrorCodes.REQUEST_TIMEOUT;
readonly errorType = 'RequestTimeoutError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class RequestEntityTooLargeError extends ServiceError {
readonly statusCode = HttpErrorCodes.REQUEST_ENTITY_TOO_LARGE;
readonly errorType = 'RequestEntityTooLargeError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class InternalServerError extends ServiceError {
readonly statusCode = HttpErrorCodes.INTERNAL_SERVER_ERROR;
readonly errorType = 'InternalServerError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}

export class ServiceUnavailableError extends ServiceError {
readonly statusCode = HttpErrorCodes.SERVICE_UNAVAILABLE;
readonly errorType = 'ServiceUnavailableError';

constructor(
message?: string,
options?: ErrorOptions,
details?: Record<string, unknown>
) {
super(message, options, details);
}
}
32 changes: 31 additions & 1 deletion packages/event-handler/src/types/rest.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
import type { BaseRouter } from '../rest/BaseRouter.js';
import type { HttpVerbs } from '../rest/constants.js';
import type { HttpErrorCodes, HttpVerbs } from '../rest/constants.js';
import type { Route } from '../rest/Route.js';

type ErrorResponse = {
statusCode: HttpStatusCode;
error: string;
message: string;
};

interface ErrorContext {
path: string;
method: string;
headers: Record<string, string>;
timestamp: string;
requestId?: string;
}

type ErrorHandler<T extends Error = Error> = (
error: T,
context?: ErrorContext
) => ErrorResponse;

interface ErrorConstructor<T extends Error = Error> {
new (...args: any[]): T;
prototype: T;
}

/**
* Options for the {@link BaseRouter} class
*/
Expand All @@ -29,6 +53,8 @@ type RouteHandler<T = any, R = any> = (...args: T[]) => R;

type HttpMethod = keyof typeof HttpVerbs;

type HttpStatusCode = (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes];

type Path = `/${string}`;

type RouteHandlerOptions = {
Expand Down Expand Up @@ -59,6 +85,10 @@ type ValidationResult = {
export type {
CompiledRoute,
DynamicRoute,
ErrorResponse,
ErrorConstructor,
ErrorHandler,
HttpStatusCode,
HttpMethod,
Path,
RouterOptions,
Expand Down
Loading
Loading