chanfana
Version:
OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!
310 lines (278 loc) • 7.58 kB
text/typescript
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;
}
}