From 94f34552c7a11d34c97e68040cec456e3b7ba0a3 Mon Sep 17 00:00:00 2001 From: svozza Date: Mon, 11 Aug 2025 12:50:23 +0100 Subject: [PATCH 1/4] add error classes for http errors --- packages/event-handler/src/rest/constants.ts | 12 + packages/event-handler/src/rest/errors.ts | 145 ++++++++++ packages/event-handler/src/types/rest.ts | 7 + .../tests/unit/rest/errors.test.ts | 257 ++++++++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 packages/event-handler/tests/unit/rest/errors.test.ts diff --git a/packages/event-handler/src/rest/constants.ts b/packages/event-handler/src/rest/constants.ts index 2f4abbd86..adc053fa2 100644 --- a/packages/event-handler/src/rest/constants.ts +++ b/packages/event-handler/src/rest/constants.ts @@ -10,6 +10,18 @@ export const HttpVerbs = { OPTIONS: 'OPTIONS', } as const; +export const HttpErrorCodes = { + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + METHOD_NOT_ALLOWED: 405, + REQUEST_TIMEOUT: 408, + REQUEST_ENTITY_TOO_LARGE: 413, + INTERNAL_SERVER_ERROR: 500, + SERVICE_UNAVAILABLE: 503, +} as const; + export const PARAM_PATTERN = /:([a-zA-Z_]\w*)(?=\/|$)/g; export const SAFE_CHARS = "-._~()'!*:@,;=+&$"; diff --git a/packages/event-handler/src/rest/errors.ts b/packages/event-handler/src/rest/errors.ts index f72339d38..8c853b762 100644 --- a/packages/event-handler/src/rest/errors.ts +++ b/packages/event-handler/src/rest/errors.ts @@ -1,3 +1,6 @@ +import type { ErrorResponse } from '../types/rest.js'; +import { HttpErrorCodes } from './constants.js'; + export class RouteMatchingError extends Error { constructor( message: string, @@ -15,3 +18,145 @@ export class ParameterValidationError extends RouteMatchingError { this.name = 'ParameterValidationError'; } } + +abstract class ServiceError extends Error { + abstract readonly statusCode: number; + abstract readonly errorType: string; + public readonly details?: Record; + + constructor( + message: string, + options?: ErrorOptions, + details?: Record + ) { + 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 + ) { + super(message ?? 'Bad request', options, details); + } +} + +export class UnauthorizedError extends ServiceError { + readonly statusCode = HttpErrorCodes.UNAUTHORIZED; + readonly errorType = 'UnauthorizedError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Unauthorized', options, details); + } +} + +export class ForbiddenError extends ServiceError { + readonly statusCode = HttpErrorCodes.FORBIDDEN; + readonly errorType = 'ForbiddenError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Forbidden', options, details); + } +} + +export class NotFoundError extends ServiceError { + readonly statusCode = HttpErrorCodes.NOT_FOUND; + readonly errorType = 'NotFoundError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Not found', options, details); + } +} + +export class MethodNotAllowedError extends ServiceError { + readonly statusCode = HttpErrorCodes.METHOD_NOT_ALLOWED; + readonly errorType = 'MethodNotAllowedError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Method not allowed', options, details); + } +} + +export class RequestTimeoutError extends ServiceError { + readonly statusCode = HttpErrorCodes.REQUEST_TIMEOUT; + readonly errorType = 'RequestTimeoutError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Request timeout', options, details); + } +} + +export class RequestEntityTooLargeError extends ServiceError { + readonly statusCode = HttpErrorCodes.REQUEST_ENTITY_TOO_LARGE; + readonly errorType = 'RequestEntityTooLargeError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Request entity too large', options, details); + } +} + +export class InternalServerError extends ServiceError { + readonly statusCode = HttpErrorCodes.INTERNAL_SERVER_ERROR; + readonly errorType = 'InternalServerError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Internal server error', options, details); + } +} + +export class ServiceUnavailableError extends ServiceError { + readonly statusCode = HttpErrorCodes.SERVICE_UNAVAILABLE; + readonly errorType = 'ServiceUnavailableError'; + + constructor( + message?: string, + options?: ErrorOptions, + details?: Record + ) { + super(message ?? 'Service unavailable', options, details); + } +} diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index 9665d2f08..1af860e34 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -3,6 +3,12 @@ import type { BaseRouter } from '../rest/BaseRouter.js'; import type { HttpVerbs } from '../rest/constants.js'; import type { Route } from '../rest/Route.js'; +type ErrorResponse = { + statusCode: number; + error: string; + message: string; +}; + /** * Options for the {@link BaseRouter} class */ @@ -59,6 +65,7 @@ type ValidationResult = { export type { CompiledRoute, DynamicRoute, + ErrorResponse, HttpMethod, Path, RouterOptions, diff --git a/packages/event-handler/tests/unit/rest/errors.test.ts b/packages/event-handler/tests/unit/rest/errors.test.ts new file mode 100644 index 000000000..0448bcaa2 --- /dev/null +++ b/packages/event-handler/tests/unit/rest/errors.test.ts @@ -0,0 +1,257 @@ +import { describe, expect, it } from 'vitest'; +import { HttpErrorCodes } from '../../../src/rest/constants.js'; +import { + BadRequestError, + ForbiddenError, + InternalServerError, + MethodNotAllowedError, + NotFoundError, + RequestEntityTooLargeError, + RequestTimeoutError, + ServiceUnavailableError, + UnauthorizedError, +} from '../../../src/rest/errors.js'; + +describe('HTTP Error Classes', () => { + it.each([ + { + ErrorClass: BadRequestError, + errorType: 'BadRequestError', + statusCode: HttpErrorCodes.BAD_REQUEST, + defaultMessage: 'Bad request', + customMessage: 'Invalid input', + }, + { + ErrorClass: UnauthorizedError, + errorType: 'UnauthorizedError', + statusCode: HttpErrorCodes.UNAUTHORIZED, + defaultMessage: 'Unauthorized', + customMessage: 'Token expired', + }, + { + ErrorClass: ForbiddenError, + errorType: 'ForbiddenError', + statusCode: HttpErrorCodes.FORBIDDEN, + defaultMessage: 'Forbidden', + customMessage: 'Access denied', + }, + { + ErrorClass: NotFoundError, + errorType: 'NotFoundError', + statusCode: HttpErrorCodes.NOT_FOUND, + defaultMessage: 'Not found', + customMessage: 'Resource not found', + }, + { + ErrorClass: MethodNotAllowedError, + errorType: 'MethodNotAllowedError', + statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, + defaultMessage: 'Method not allowed', + customMessage: 'POST not allowed', + }, + { + ErrorClass: RequestTimeoutError, + errorType: 'RequestTimeoutError', + statusCode: HttpErrorCodes.REQUEST_TIMEOUT, + defaultMessage: 'Request timeout', + customMessage: 'Operation timed out', + }, + { + ErrorClass: RequestEntityTooLargeError, + errorType: 'RequestEntityTooLargeError', + statusCode: HttpErrorCodes.REQUEST_ENTITY_TOO_LARGE, + defaultMessage: 'Request entity too large', + customMessage: 'File too large', + }, + { + ErrorClass: InternalServerError, + errorType: 'InternalServerError', + statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, + defaultMessage: 'Internal server error', + customMessage: 'Database connection failed', + }, + { + ErrorClass: ServiceUnavailableError, + errorType: 'ServiceUnavailableError', + statusCode: HttpErrorCodes.SERVICE_UNAVAILABLE, + defaultMessage: 'Service unavailable', + customMessage: 'Maintenance mode', + }, + ])( + '$errorType uses default message when none provided', + ({ ErrorClass, errorType, statusCode, defaultMessage }) => { + const error = new ErrorClass(); + expect(error.message).toBe(defaultMessage); + expect(error.statusCode).toBe(statusCode); + expect(error.errorType).toBe(errorType); + } + ); + + it.each([ + { + ErrorClass: BadRequestError, + errorType: 'BadRequestError', + statusCode: HttpErrorCodes.BAD_REQUEST, + customMessage: 'Invalid input', + }, + { + ErrorClass: UnauthorizedError, + errorType: 'UnauthorizedError', + statusCode: HttpErrorCodes.UNAUTHORIZED, + customMessage: 'Token expired', + }, + { + ErrorClass: ForbiddenError, + errorType: 'ForbiddenError', + statusCode: HttpErrorCodes.FORBIDDEN, + customMessage: 'Access denied', + }, + { + ErrorClass: NotFoundError, + errorType: 'NotFoundError', + statusCode: HttpErrorCodes.NOT_FOUND, + customMessage: 'Resource not found', + }, + { + ErrorClass: MethodNotAllowedError, + errorType: 'MethodNotAllowedError', + statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, + customMessage: 'POST not allowed', + }, + { + ErrorClass: RequestTimeoutError, + errorType: 'RequestTimeoutError', + statusCode: HttpErrorCodes.REQUEST_TIMEOUT, + customMessage: 'Operation timed out', + }, + { + ErrorClass: RequestEntityTooLargeError, + errorType: 'RequestEntityTooLargeError', + statusCode: HttpErrorCodes.REQUEST_ENTITY_TOO_LARGE, + customMessage: 'File too large', + }, + { + ErrorClass: InternalServerError, + errorType: 'InternalServerError', + statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, + customMessage: 'Database connection failed', + }, + { + ErrorClass: ServiceUnavailableError, + errorType: 'ServiceUnavailableError', + statusCode: HttpErrorCodes.SERVICE_UNAVAILABLE, + customMessage: 'Maintenance mode', + }, + ])( + '$errorType uses custom message when provided', + ({ ErrorClass, errorType, statusCode, customMessage }) => { + const error = new ErrorClass(customMessage); + expect(error.message).toBe(customMessage); + expect(error.statusCode).toBe(statusCode); + expect(error.errorType).toBe(errorType); + } + ); + + describe('toJSON', () => { + it.each([ + { + ErrorClass: BadRequestError, + errorType: 'BadRequestError', + statusCode: HttpErrorCodes.BAD_REQUEST, + message: 'Invalid input', + }, + { + ErrorClass: UnauthorizedError, + errorType: 'UnauthorizedError', + statusCode: HttpErrorCodes.UNAUTHORIZED, + message: 'Token expired', + }, + { + ErrorClass: ForbiddenError, + errorType: 'ForbiddenError', + statusCode: HttpErrorCodes.FORBIDDEN, + message: 'Access denied', + }, + { + ErrorClass: NotFoundError, + errorType: 'NotFoundError', + statusCode: HttpErrorCodes.NOT_FOUND, + message: 'Resource not found', + }, + { + ErrorClass: MethodNotAllowedError, + errorType: 'MethodNotAllowedError', + statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, + message: 'POST not allowed', + }, + { + ErrorClass: RequestTimeoutError, + errorType: 'RequestTimeoutError', + statusCode: HttpErrorCodes.REQUEST_TIMEOUT, + message: 'Operation timed out', + }, + { + ErrorClass: RequestEntityTooLargeError, + errorType: 'RequestEntityTooLargeError', + statusCode: HttpErrorCodes.REQUEST_ENTITY_TOO_LARGE, + message: 'File too large', + }, + { + ErrorClass: InternalServerError, + errorType: 'InternalServerError', + statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, + message: 'Database connection failed', + }, + { + ErrorClass: ServiceUnavailableError, + errorType: 'ServiceUnavailableError', + statusCode: HttpErrorCodes.SERVICE_UNAVAILABLE, + message: 'Maintenance mode', + }, + ])( + '$errorType serializes to JSON format', + ({ ErrorClass, errorType, statusCode, message }) => { + const error = new ErrorClass(message); + const json = error.toJSON(); + + expect(json).toEqual({ + statusCode, + error: errorType, + message, + }); + } + ); + + it('includes details in JSON when provided', () => { + const details = { field: 'value', code: 'VALIDATION_ERROR' }; + const error = new BadRequestError('Invalid input', undefined, details); + const json = error.toJSON(); + + expect(json).toEqual({ + statusCode: HttpErrorCodes.BAD_REQUEST, + error: 'BadRequestError', + message: 'Invalid input', + details, + }); + }); + + it('excludes details from JSON when not provided', () => { + const error = new BadRequestError('Invalid input'); + const json = error.toJSON(); + + expect(json).toEqual({ + statusCode: HttpErrorCodes.BAD_REQUEST, + error: 'BadRequestError', + message: 'Invalid input', + }); + expect(json).not.toHaveProperty('details'); + }); + }); + + it('passes options to Error superclass', () => { + const cause = new Error('Root cause'); + const error = new BadRequestError('Invalid input', { cause }); + + expect(error.cause).toBe(cause); + }); +}); From bc78e509093a9023006f7115e5de73ad5bcb6eaf Mon Sep 17 00:00:00 2001 From: svozza Date: Mon, 11 Aug 2025 15:12:10 +0100 Subject: [PATCH 2/4] add all http error codes --- packages/event-handler/src/rest/constants.ts | 62 ++++++++++++++++++++ packages/event-handler/src/types/rest.ts | 26 +++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/rest/constants.ts b/packages/event-handler/src/rest/constants.ts index adc053fa2..c319f8850 100644 --- a/packages/event-handler/src/rest/constants.ts +++ b/packages/event-handler/src/rest/constants.ts @@ -11,15 +11,77 @@ export const HttpVerbs = { } 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; diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index 1af860e34..f360771a0 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -1,14 +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: number; + statusCode: HttpStatusCode; error: string; message: string; }; +interface ErrorContext { + path: string; + method: string; + headers: Record; + timestamp: string; + requestId?: string; +} + +type ErrorHandler = ( + error: T, + context?: ErrorContext +) => ErrorResponse; + +interface ErrorConstructor { + new (...args: any[]): T; + prototype: T; +} + /** * Options for the {@link BaseRouter} class */ @@ -35,6 +53,8 @@ type RouteHandler = (...args: T[]) => R; type HttpMethod = keyof typeof HttpVerbs; +type HttpStatusCode = (typeof HttpErrorCodes)[keyof typeof HttpErrorCodes]; + type Path = `/${string}`; type RouteHandlerOptions = { @@ -66,6 +86,8 @@ export type { CompiledRoute, DynamicRoute, ErrorResponse, + ErrorConstructor, + ErrorHandler, HttpMethod, Path, RouterOptions, From 066d25a7b73bc8a82462f95911f877b84affdfde Mon Sep 17 00:00:00 2001 From: svozza Date: Mon, 11 Aug 2025 15:16:31 +0100 Subject: [PATCH 3/4] fix type error --- packages/event-handler/src/rest/errors.ts | 4 ++-- packages/event-handler/src/types/rest.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/rest/errors.ts b/packages/event-handler/src/rest/errors.ts index 8c853b762..a40450fd0 100644 --- a/packages/event-handler/src/rest/errors.ts +++ b/packages/event-handler/src/rest/errors.ts @@ -1,4 +1,4 @@ -import type { ErrorResponse } from '../types/rest.js'; +import type { ErrorResponse, HttpStatusCode } from '../types/rest.js'; import { HttpErrorCodes } from './constants.js'; export class RouteMatchingError extends Error { @@ -20,7 +20,7 @@ export class ParameterValidationError extends RouteMatchingError { } abstract class ServiceError extends Error { - abstract readonly statusCode: number; + abstract readonly statusCode: HttpStatusCode; abstract readonly errorType: string; public readonly details?: Record; diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index f360771a0..702862dd6 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -88,6 +88,7 @@ export type { ErrorResponse, ErrorConstructor, ErrorHandler, + HttpStatusCode, HttpMethod, Path, RouterOptions, From 3be32f2445dabd7abf03e4fab4913a3ab39d40a2 Mon Sep 17 00:00:00 2001 From: svozza Date: Mon, 11 Aug 2025 18:39:11 +0100 Subject: [PATCH 4/4] make message field optional in ServiceError --- packages/event-handler/src/rest/errors.ts | 20 ++--- .../tests/unit/rest/errors.test.ts | 74 ------------------- 2 files changed, 10 insertions(+), 84 deletions(-) diff --git a/packages/event-handler/src/rest/errors.ts b/packages/event-handler/src/rest/errors.ts index a40450fd0..426566d08 100644 --- a/packages/event-handler/src/rest/errors.ts +++ b/packages/event-handler/src/rest/errors.ts @@ -25,7 +25,7 @@ abstract class ServiceError extends Error { public readonly details?: Record; constructor( - message: string, + message?: string, options?: ErrorOptions, details?: Record ) { @@ -53,7 +53,7 @@ export class BadRequestError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Bad request', options, details); + super(message, options, details); } } @@ -66,7 +66,7 @@ export class UnauthorizedError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Unauthorized', options, details); + super(message, options, details); } } @@ -79,7 +79,7 @@ export class ForbiddenError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Forbidden', options, details); + super(message, options, details); } } @@ -92,7 +92,7 @@ export class NotFoundError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Not found', options, details); + super(message, options, details); } } @@ -105,7 +105,7 @@ export class MethodNotAllowedError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Method not allowed', options, details); + super(message, options, details); } } @@ -118,7 +118,7 @@ export class RequestTimeoutError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Request timeout', options, details); + super(message, options, details); } } @@ -131,7 +131,7 @@ export class RequestEntityTooLargeError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Request entity too large', options, details); + super(message, options, details); } } @@ -144,7 +144,7 @@ export class InternalServerError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Internal server error', options, details); + super(message, options, details); } } @@ -157,6 +157,6 @@ export class ServiceUnavailableError extends ServiceError { options?: ErrorOptions, details?: Record ) { - super(message ?? 'Service unavailable', options, details); + super(message, options, details); } } diff --git a/packages/event-handler/tests/unit/rest/errors.test.ts b/packages/event-handler/tests/unit/rest/errors.test.ts index 0448bcaa2..becacd2b8 100644 --- a/packages/event-handler/tests/unit/rest/errors.test.ts +++ b/packages/event-handler/tests/unit/rest/errors.test.ts @@ -13,80 +13,6 @@ import { } from '../../../src/rest/errors.js'; describe('HTTP Error Classes', () => { - it.each([ - { - ErrorClass: BadRequestError, - errorType: 'BadRequestError', - statusCode: HttpErrorCodes.BAD_REQUEST, - defaultMessage: 'Bad request', - customMessage: 'Invalid input', - }, - { - ErrorClass: UnauthorizedError, - errorType: 'UnauthorizedError', - statusCode: HttpErrorCodes.UNAUTHORIZED, - defaultMessage: 'Unauthorized', - customMessage: 'Token expired', - }, - { - ErrorClass: ForbiddenError, - errorType: 'ForbiddenError', - statusCode: HttpErrorCodes.FORBIDDEN, - defaultMessage: 'Forbidden', - customMessage: 'Access denied', - }, - { - ErrorClass: NotFoundError, - errorType: 'NotFoundError', - statusCode: HttpErrorCodes.NOT_FOUND, - defaultMessage: 'Not found', - customMessage: 'Resource not found', - }, - { - ErrorClass: MethodNotAllowedError, - errorType: 'MethodNotAllowedError', - statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED, - defaultMessage: 'Method not allowed', - customMessage: 'POST not allowed', - }, - { - ErrorClass: RequestTimeoutError, - errorType: 'RequestTimeoutError', - statusCode: HttpErrorCodes.REQUEST_TIMEOUT, - defaultMessage: 'Request timeout', - customMessage: 'Operation timed out', - }, - { - ErrorClass: RequestEntityTooLargeError, - errorType: 'RequestEntityTooLargeError', - statusCode: HttpErrorCodes.REQUEST_ENTITY_TOO_LARGE, - defaultMessage: 'Request entity too large', - customMessage: 'File too large', - }, - { - ErrorClass: InternalServerError, - errorType: 'InternalServerError', - statusCode: HttpErrorCodes.INTERNAL_SERVER_ERROR, - defaultMessage: 'Internal server error', - customMessage: 'Database connection failed', - }, - { - ErrorClass: ServiceUnavailableError, - errorType: 'ServiceUnavailableError', - statusCode: HttpErrorCodes.SERVICE_UNAVAILABLE, - defaultMessage: 'Service unavailable', - customMessage: 'Maintenance mode', - }, - ])( - '$errorType uses default message when none provided', - ({ ErrorClass, errorType, statusCode, defaultMessage }) => { - const error = new ErrorClass(); - expect(error.message).toBe(defaultMessage); - expect(error.statusCode).toBe(statusCode); - expect(error.errorType).toBe(errorType); - } - ); - it.each([ { ErrorClass: BadRequestError,