Files

205 lines
5.6 KiB
TypeScript

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/** A collection of HTTP errors and utilities.
*
* The export {@linkcode errors} contains an individual class that extends
* {@linkcode HttpError} which makes handling HTTP errors in a structured way.
*
* The function {@linkcode createHttpError} provides a way to create instances
* of errors in a factory pattern.
*
* The function {@linkcode isHttpError} is a type guard that will narrow a value
* to an `HttpError` instance.
*
* @example
* ```ts
* import { errors, isHttpError } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
*
* try {
* throw new errors.NotFound();
* } catch (e) {
* if (isHttpError(e)) {
* const response = new Response(e.message, { status: e.status });
* } else {
* throw e;
* }
* }
* ```
*
* @example
* ```ts
* import { createHttpError } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
* import { Status } from "https://deno.land/std@$STD_VERSION/http/http_status.ts";
*
* try {
* throw createHttpError(
* Status.BadRequest,
* "The request was bad.",
* { expose: false }
* );
* } catch (e) {
* // handle errors
* }
* ```
*
* @module
*/
import {
type ErrorStatus,
isClientErrorStatus,
Status,
STATUS_TEXT,
} from "./http_status.ts";
const ERROR_STATUS_MAP = {
"BadRequest": 400,
"Unauthorized": 401,
"PaymentRequired": 402,
"Forbidden": 403,
"NotFound": 404,
"MethodNotAllowed": 405,
"NotAcceptable": 406,
"ProxyAuthRequired": 407,
"RequestTimeout": 408,
"Conflict": 409,
"Gone": 410,
"LengthRequired": 411,
"PreconditionFailed": 412,
"RequestEntityTooLarge": 413,
"RequestURITooLong": 414,
"UnsupportedMediaType": 415,
"RequestedRangeNotSatisfiable": 416,
"ExpectationFailed": 417,
"Teapot": 418,
"MisdirectedRequest": 421,
"UnprocessableEntity": 422,
"Locked": 423,
"FailedDependency": 424,
"UpgradeRequired": 426,
"PreconditionRequired": 428,
"TooManyRequests": 429,
"RequestHeaderFieldsTooLarge": 431,
"UnavailableForLegalReasons": 451,
"InternalServerError": 500,
"NotImplemented": 501,
"BadGateway": 502,
"ServiceUnavailable": 503,
"GatewayTimeout": 504,
"HTTPVersionNotSupported": 505,
"VariantAlsoNegotiates": 506,
"InsufficientStorage": 507,
"LoopDetected": 508,
"NotExtended": 510,
"NetworkAuthenticationRequired": 511,
} as const;
export type ErrorStatusKeys = keyof typeof ERROR_STATUS_MAP;
export interface HttpErrorOptions extends ErrorOptions {
expose?: boolean;
headers?: HeadersInit;
}
/** The base class that all derivative HTTP extend, providing a `status` and an
* `expose` property. */
export class HttpError extends Error {
#status: ErrorStatus = Status.InternalServerError;
#expose: boolean;
#headers?: Headers;
constructor(
message = "Http Error",
options?: HttpErrorOptions,
) {
super(message, options);
this.#expose = options?.expose === undefined
? isClientErrorStatus(this.status)
: options.expose;
if (options?.headers) {
this.#headers = new Headers(options.headers);
}
}
/** A flag to indicate if the internals of the error, like the stack, should
* be exposed to a client, or if they are "private" and should not be leaked.
* By default, all client errors are `true` and all server errors are
* `false`. */
get expose(): boolean {
return this.#expose;
}
/** The optional headers object that is set on the error. */
get headers(): Headers | undefined {
return this.#headers;
}
/** The error status that is set on the error. */
get status(): ErrorStatus {
return this.#status;
}
}
function createHttpErrorConstructor(status: ErrorStatus): typeof HttpError {
const name = `${Status[status]}Error`;
const ErrorCtor = class extends HttpError {
constructor(
message = STATUS_TEXT[status],
options?: HttpErrorOptions,
) {
super(message, options);
Object.defineProperty(this, "name", {
configurable: true,
enumerable: false,
value: name,
writable: true,
});
}
override get status() {
return status;
}
};
return ErrorCtor;
}
/**
* A namespace that contains each error constructor. Each error extends
* `HTTPError` and provides `.status` and `.expose` properties, where the
* `.status` will be an error `Status` value and `.expose` indicates if
* information, like a stack trace, should be shared in the response.
*
* By default, `.expose` is set to false in server errors, and true for client
* errors.
*
* @example
* ```ts
* import { errors } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
*
* throw new errors.InternalServerError("Ooops!");
* ```
*/
export const errors: Record<ErrorStatusKeys, typeof HttpError> = {} as Record<
ErrorStatusKeys,
typeof HttpError
>;
for (const [key, value] of Object.entries(ERROR_STATUS_MAP)) {
errors[key as ErrorStatusKeys] = createHttpErrorConstructor(value);
}
/**
* A factory function which provides a way to create errors. It takes up to 3
* arguments, the error `Status`, an message, which defaults to the status text
* and error options, which incudes the `expose` property to set the `.expose`
* value on the error.
*/
export function createHttpError(
status: ErrorStatus = Status.InternalServerError,
message?: string,
options?: HttpErrorOptions,
): HttpError {
return new errors[Status[status] as ErrorStatusKeys](message, options);
}
/** A type guard that determines if the value is an HttpError or not. */
export function isHttpError(value: unknown): value is HttpError {
return value instanceof HttpError;
}