UNPKG

chanfana

Version:

OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!

310 lines (278 loc) 7.58 kB
import { z } from "zod"; import { contentJson } from "./contentTypes"; /** * Base exception class for API errors. * Extend this class to create custom API exceptions with specific status codes and error codes. * * @example * ```typescript * throw new ApiException("Something went wrong"); * ``` */ export class ApiException extends Error { isVisible = true; message: string; default_message = "Internal Error"; status = 500; code = 7000; includesPath = false; constructor(message = "") { super(message); this.message = message; } buildResponse() { return [ { code: this.code, message: this.isVisible ? this.message || this.default_message : "Internal Error", }, ]; } static schema() { const inst = new this(); const errorSchema = inst.includesPath ? z.object({ code: z.number(), message: z.string(), path: z.array(z.string()), }) : z.object({ code: z.number(), message: z.string(), }); return { [inst.status]: { description: inst.default_message, ...contentJson( z.object({ success: z.literal(false), errors: z.array(errorSchema), }), ), }, }; } } /** * Exception for input validation errors (400). * Used when request data fails Zod schema validation. * * @example * ```typescript * throw new InputValidationException("Invalid email format", ["body", "email"]); * ``` */ export class InputValidationException extends ApiException { isVisible = true; default_message = "Input Validation Error"; status = 400; code = 7001; path = null; includesPath = true; constructor(message?: string, path?: any) { super(message); this.path = path; } buildResponse() { return [ { code: this.code, message: this.isVisible ? this.message : "Internal Error", path: this.path, }, ]; } } /** * Exception that aggregates multiple API exceptions. * The highest status code among all errors will be used as the response status. * * @example * ```typescript * throw new MultiException([ * new InputValidationException("Invalid email", ["body", "email"]), * new InputValidationException("Invalid name", ["body", "name"]), * ]); * ``` */ export class MultiException extends ApiException { isVisible = true; errors: Array<ApiException>; status = 400; constructor(errors: Array<ApiException>) { super("Multiple Exceptions"); this.errors = errors; // Because the API can only return 1 status code, always return the highest for (const err of errors) { if (err.status > this.status) { this.status = err.status; } if (!err.isVisible && this.isVisible) { this.isVisible = false; } } } buildResponse() { return this.errors.flatMap((err) => err.buildResponse()); } } /** * Exception for resource not found (404). * Used when the requested resource doesn't exist. * * @example * ```typescript * throw new NotFoundException("User not found"); * ``` */ export class NotFoundException extends ApiException { isVisible = true; default_message = "Not Found"; status = 404; code = 7002; } /** * Exception for unauthorized access (401). * Used when authentication is required but not provided or invalid. */ export class UnauthorizedException extends ApiException { isVisible = true; default_message = "Unauthorized"; status = 401; code = 7003; } /** * Exception for forbidden access (403). * Used when the user is authenticated but doesn't have permission. */ export class ForbiddenException extends ApiException { isVisible = true; default_message = "Forbidden"; status = 403; code = 7004; } /** * Exception for method not allowed (405). * Used when the HTTP method is not supported for the requested resource. */ export class MethodNotAllowedException extends ApiException { isVisible = true; default_message = "Method Not Allowed"; status = 405; code = 7005; } /** * Exception for conflict errors (409). * Used when the request conflicts with the current state (e.g., duplicate resource). */ export class ConflictException extends ApiException { isVisible = true; default_message = "Conflict"; status = 409; code = 7006; } /** * Exception for unprocessable entity (422). * Used when the request is well-formed but semantically incorrect. */ export class UnprocessableEntityException extends ApiException { isVisible = true; default_message = "Unprocessable Entity"; status = 422; code = 7007; includesPath = true; path: any = null; constructor(message?: string, path?: any) { super(message); this.path = path; } buildResponse() { return [ { code: this.code, message: this.isVisible ? this.message || this.default_message : "Internal Error", path: this.path, }, ]; } } /** * Exception for rate limiting (429). * Used when the user has sent too many requests in a given time period. */ export class TooManyRequestsException extends ApiException { isVisible = true; default_message = "Too Many Requests"; status = 429; code = 7008; retryAfter?: number; constructor(message?: string, retryAfter?: number) { super(message); this.retryAfter = retryAfter; } } /** * Exception for unexpected server errors (500). * Unlike the base ApiException (also 500, code 7000), this class defaults to isVisible=false, * meaning the error message is hidden from clients and replaced with "Internal Error". * Use this for errors that should be logged but not exposed to end users. * Use the base ApiException when the error message is safe to expose. */ export class InternalServerErrorException extends ApiException { isVisible = false; default_message = "Internal Server Error"; status = 500; code = 7009; } /** * Exception for bad gateway errors (502). * Used when an upstream server returns an invalid response. */ export class BadGatewayException extends ApiException { isVisible = true; default_message = "Bad Gateway"; status = 502; code = 7010; } /** * Exception for service unavailable (503). * Used when the server is temporarily unavailable (maintenance, overload). */ export class ServiceUnavailableException extends ApiException { isVisible = true; default_message = "Service Unavailable"; status = 503; code = 7011; retryAfter?: number; constructor(message?: string, retryAfter?: number) { super(message); this.retryAfter = retryAfter; } } /** * Exception for gateway timeout (504). * Used when an upstream server doesn't respond in time. */ export class GatewayTimeoutException extends ApiException { isVisible = true; default_message = "Gateway Timeout"; status = 504; code = 7012; } /** * Exception for response validation errors (500). * Used when a handler's response doesn't match its declared Zod response schema. * This is a server-side bug (the handler produced invalid data), not a client error. * Error details are hidden from clients (isVisible=false) and logged via console.error. */ export class ResponseValidationException extends ApiException { isVisible = false; default_message = "Response Validation Error"; status = 500; code = 7013; constructor(message?: string, options?: ErrorOptions) { super(message ?? ""); if (message) this.message = message; if (options?.cause) this.cause = options.cause; } }