diff --git a/src/index.ts b/src/index.ts index 1ffd311..a2a9bf6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { ZodRawShape, ZodTypeAny, } from 'zod' -import { HTTPError, HTTPStatus, HTTPStatusText } from './utils/error.js' +import { ErrorReturnType, HTTPError, HTTPStatus, HTTPStatusText } from './utils/error.js' export { HTTPError, HTTPStatus, HTTPStatusText } from './utils/error.js' export type CustomRequest< @@ -43,7 +43,7 @@ type Middleware< class ValidationError extends Error { public statusCode: number - public errors: { message: string; path: string[] }[] + public errors: ErrorReturnType['errors'] public location: string constructor(zodError: ZodError, location: string) { @@ -315,8 +315,15 @@ export class Handler< return } } catch (err) { - next(err) - return + if (err instanceof HTTPError) { + return res.status(err.status).json({ + errors: [{ message: err.message }], + type: HTTPStatusText[err.status as HTTPStatus], + } satisfies ErrorReturnType) + } else { + next(err) + return + } } } @@ -354,12 +361,12 @@ export class Handler< errors: err.errors, location: err.location, type: HTTPStatusText[err.statusCode as HTTPStatus], - }) + } satisfies ErrorReturnType) } else if (err instanceof HTTPError) { return res.status(err.status).json({ - errors: [err.message], + errors: [{ message: err.message }], type: HTTPStatusText[err.status as HTTPStatus], - }) + } satisfies ErrorReturnType) } else { // call the native next express error handler next(err) diff --git a/src/utils/error.ts b/src/utils/error.ts index 6a5bb9d..f4abdf2 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1,26 +1,144 @@ export enum HTTPStatus { 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, + + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + SWITCH_PROXY = 306, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + 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, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + I_AM_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, + 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, } export const HTTPStatusText: Record = { [HTTPStatus.OK]: 'OK', + [HTTPStatus.CREATED]: 'Created', + [HTTPStatus.ACCEPTED]: 'Accepted', + [HTTPStatus.NON_AUTHORITATIVE_INFORMATION]: 'Non-Authoritative Information', + [HTTPStatus.NO_CONTENT]: 'No Content', + [HTTPStatus.RESET_CONTENT]: 'Reset Content', + [HTTPStatus.PARTIAL_CONTENT]: 'Partial Content', + [HTTPStatus.MULTI_STATUS]: 'Multi-Status', + [HTTPStatus.ALREADY_REPORTED]: 'Already Reported', + [HTTPStatus.IM_USED]: 'IM Used', + + [HTTPStatus.MULTIPLE_CHOICES]: 'Multiple Choices', + [HTTPStatus.MOVED_PERMANENTLY]: 'Moved Permanently', + [HTTPStatus.FOUND]: 'Found', + [HTTPStatus.SEE_OTHER]: 'See Other', + [HTTPStatus.NOT_MODIFIED]: 'Not Modified', + [HTTPStatus.USE_PROXY]: 'Use Proxy', + [HTTPStatus.SWITCH_PROXY]: 'Switch Proxy', + [HTTPStatus.TEMPORARY_REDIRECT]: 'Temporary Redirect', + [HTTPStatus.PERMANENT_REDIRECT]: 'Permanent Redirect', + [HTTPStatus.BAD_REQUEST]: 'Bad Request', [HTTPStatus.UNAUTHORIZED]: 'Unauthorized', + [HTTPStatus.PAYMENT_REQUIRED]: 'Payment Required', [HTTPStatus.FORBIDDEN]: 'Forbidden', [HTTPStatus.NOT_FOUND]: 'Not Found', + [HTTPStatus.METHOD_NOT_ALLOWED]: 'Method Not Allowed', + [HTTPStatus.NOT_ACCEPTABLE]: 'Not Acceptable', + [HTTPStatus.PROXY_AUTHENTICATION_REQUIRED]: 'Proxy Authentication Required', + [HTTPStatus.REQUEST_TIMEOUT]: 'Request Timeout', + [HTTPStatus.CONFLICT]: 'Conflict', + [HTTPStatus.GONE]: 'Gone', + [HTTPStatus.LENGTH_REQUIRED]: 'Length Required', + [HTTPStatus.PRECONDITION_FAILED]: 'Precondition Failed', + [HTTPStatus.PAYLOAD_TOO_LARGE]: 'Payload Too Large', + [HTTPStatus.URI_TOO_LONG]: 'URI Too Long', + [HTTPStatus.UNSUPPORTED_MEDIA_TYPE]: 'Unsupported Media Type', + [HTTPStatus.RANGE_NOT_SATISFIABLE]: 'Range Not Satisfiable', + [HTTPStatus.EXPECTATION_FAILED]: 'Expectation Failed', + [HTTPStatus.I_AM_A_TEAPOT]: "I'm a teapot", + [HTTPStatus.MISDIRECTED_REQUEST]: 'Misdirected Request', + [HTTPStatus.UNPROCESSABLE_ENTITY]: 'Unprocessable Entity', + [HTTPStatus.LOCKED]: 'Locked', + [HTTPStatus.FAILED_DEPENDENCY]: 'Failed Dependency', + [HTTPStatus.TOO_EARLY]: 'Too Early', + [HTTPStatus.UPGRADE_REQUIRED]: 'Upgrade Required', + [HTTPStatus.PRECONDITION_REQUIRED]: 'Precondition Required', + [HTTPStatus.TOO_MANY_REQUESTS]: 'Too Many Requests', + [HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE]: 'Request Header Fields Too Large', + [HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS]: 'Unavailable For Legal Reasons', + [HTTPStatus.INTERNAL_SERVER_ERROR]: 'Internal Server Error', + [HTTPStatus.NOT_IMPLEMENTED]: 'Not Implemented', + [HTTPStatus.BAD_GATEWAY]: 'Bad Gateway', + [HTTPStatus.SERVICE_UNAVAILABLE]: 'Service Unavailable', + [HTTPStatus.GATEWAY_TIMEOUT]: 'Gateway Timeout', + [HTTPStatus.HTTP_VERSION_NOT_SUPPORTED]: 'HTTP Version Not Supported', + [HTTPStatus.VARIANT_ALSO_NEGOTIATES]: 'Variant Also Negotiates', + [HTTPStatus.INSUFFICIENT_STORAGE]: 'Insufficient Storage', + [HTTPStatus.LOOP_DETECTED]: 'Loop Detected', + [HTTPStatus.NOT_EXTENDED]: 'Not Extended', + [HTTPStatus.NETWORK_AUTHENTICATION_REQUIRED]: 'Network Authentication Required', } export class HTTPError extends Error { - constructor( - public readonly status: number, - message: string - ) { + constructor(public readonly status: number, message: string) { super(message) } } + +export type ErrorReturnType = { + errors: { + message: string + path?: string[] + }[] + location?: string + type: string +} diff --git a/tests/errors/0_errors.test.ts b/tests/errors/0_errors.test.ts new file mode 100644 index 0000000..52ec8ff --- /dev/null +++ b/tests/errors/0_errors.test.ts @@ -0,0 +1,55 @@ +import { z } from 'zod' + +import request from 'supertest' +import express from 'express' +import { Handler } from '../../src/index.js' +import { HTTPError } from '../../src/utils/error.js' + +const app = express() + +const resolverErrorHandler = new Handler() + .validate('params', { userId: z.string() }) + .resolve(async (req, res, context) => { + throw new HTTPError(400, 'This should fail') + }) + .transform((data) => { + return { + data: { + name: 'Hello World', + }, + meta: {}, + } + }) + .express() + +app.get('/user/:userId', resolverErrorHandler) + +// ALWAYS APPEND ERROR HANDLER AFTER ROUTES +app.use((err: any, req: any, res: any, next: any) => { + res.status(500).send('Something broke!') +}) + +// TESTS + +describe('Error Tests', () => { + describe('Resolver Error', () => { + it('should return 400 ', (done) => { + request(app) + .get('/user/1') + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + errors: [ + { + message: 'This should fail', + }, + ], + type: 'Bad Request', + }) + + done() + }) + .catch(done) + }) + }) +})