UNPKG

@transformgovsg/zing

Version:
1,124 lines (1,114 loc) 32.2 kB
// src/zing.ts import { createServer as createHTTPServer } from "node:http"; // src/errors.ts var BaseError = class extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; var ContentTooLargeError = class extends BaseError { type = "CONTENT_TOO_LARGE_ERROR"; constructor() { super("The request content is too large."); } }; var UnsupportedContentTypeError = class extends BaseError { type = "UNSUPPORTED_CONTENT_TYPE_ERROR"; constructor() { super("The request content type is not supported."); } }; var MalformedJSONError = class extends BaseError { type = "MALFORMED_JSON_ERROR"; constructor() { super("The request payload is not a valid JSON object."); } }; var InternalServerError = class extends BaseError { type = "INTERNAL_SERVER_ERROR"; constructor(cause) { super("An unexpected error occurred. Check the cause property for more details."); this.cause = cause; } }; // src/http-status-code.ts var HTTPStatusCode = { Continue: 100, SwitchingProtocols: 101, Processing: 102, EarlyHints: 103, OK: 200, Created: 201, Accepted: 202, NonAuthoritativeInformation: 203, NoContent: 204, ResetContent: 205, PartialContent: 206, MultiStatus: 207, AlreadyReported: 208, IMUsed: 226, MultipleChoices: 300, MovedPermanently: 301, Found: 302, SeeOther: 303, NotModified: 304, TemporaryRedirect: 307, PermanentRedirect: 308, BadRequest: 400, Unauthorized: 401, PaymentRequired: 402, Forbidden: 403, NotFound: 404, MethodNotAllowed: 405, NotAcceptable: 406, ProxyAuthenticationRequired: 407, RequestTimeout: 408, Conflict: 409, Gone: 410, LengthRequired: 411, PreconditionFailed: 412, ContentTooLarge: 413, URITooLong: 414, UnsupportedMediaType: 415, RangeNotSatisfiable: 416, ExpectationFailed: 417, ImATeapot: 418, MisdirectedRequest: 421, UnprocessableContent: 422, Locked: 423, FailedDependency: 424, TooEarly: 425, 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 }; // src/options.ts var DEFAULT_OPTIONS = { maxBodySize: 1048576 }; // src/cookie.ts var VALID_COOKIE_NAME_REGEX = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/; var VALID_COOKIE_VALUE_REGEX = /^[\u0021-\u003A\u003C-\u007E]*$/; function parse(cookie, name) { if (cookie.indexOf(name) === -1) { return null; } for (let kv of cookie.trim().split(";")) { kv = kv.trim(); const equalPos = kv.indexOf("="); if (equalPos === -1) { continue; } const key = kv.slice(0, equalPos).trim(); if (key !== name || !VALID_COOKIE_NAME_REGEX.test(key)) { continue; } let value = kv.slice(equalPos + 1).trim(); if (value.startsWith('"') && value.endsWith('"')) { value = value.slice(1, -1); } if (!VALID_COOKIE_VALUE_REGEX.test(value)) { continue; } return decodeURIComponent(value); } return null; } function serialise(name, value, options) { let cookie = `${name}=${encodeURIComponent(value)}`; if (options?.path) { cookie += `; Path=${options.path}`; } if (options?.domain) { cookie += `; Domain=${options.domain}`; } if (options?.expires) { cookie += `; Expires=${options.expires.toUTCString()}`; } if (options?.maxAge) { if (options.maxAge > 0) { cookie += `; Max-Age=${options.maxAge | 0}`; } if (options.maxAge < 0) { cookie += "; Max-Age=0"; } } if (options?.secure) { cookie += "; Secure"; } if (options?.httpOnly) { cookie += "; HttpOnly"; } switch (options?.sameSite) { case "strict": cookie += "; SameSite=Strict"; break; case "lax": cookie += "; SameSite=Lax"; break; case "none": cookie += "; SameSite=None"; break; } return cookie; } // src/result.ts var Ok = class { constructor(value) { this.value = value; } isOk() { return true; } isErr() { return !this.isOk(); } unwrap() { return this.value; } unwrapOr(_defaultValue) { return this.value; } }; var Err = class { constructor(error) { this.error = error; } isOk() { return false; } isErr() { return !this.isOk(); } unwrap() { throw this.error; } unwrapOr(defaultValue) { return defaultValue; } }; function OK(value) { return new Ok(value); } function ERR(error) { return new Err(error); } // src/request.ts var ALLOWED_HTTP_METHODS_WITH_BODY = ["PATCH", "POST", "PUT"]; var Request = class { node; #kv = /* @__PURE__ */ new Map(); #body = null; #options; #url; constructor(req, options) { this.node = req; this.#options = options; const protocol = req.socket && "encrypted" in req.socket && req.socket.encrypted ? "https" : "http"; this.#url = new URL(req.url, `${protocol}://${req.headers.host}`); } /** * Returns the protocol of the request. */ get protocol() { return this.#url.protocol === "http:" ? "http" : "https"; } /** * Returns the pathname of the request. */ get pathname() { return this.#url.pathname; } /** * Returns the HTTP method of the request. */ get method() { return this.node.method; } /** * Returns the value of the given key from the request-scoped key-value * store. If the key is not found and no default value is provided, `null` * is returned. * * @param key - The key to get the value of. * @param defaultValue - An optional default value to return if the key is not found. */ get(key, defaultValue) { const value = this.#kv.get(key); if (!value) { return defaultValue ?? null; } return value; } /** * Stores a value in the request-scoped key-value store. The store persists * only for the duration of the current request. If the value is `undefined` * or `null`, the key is removed from the store. * * @param key - The key to store the value under. * @param value - The value to store. */ set(key, value) { if (value === void 0 || value === null) { this.#kv.delete(key); return; } this.#kv.set(key, value); } /** * Returns the value of the given cookie name from the request. If the * cookie is not found and no default value is provided, `null` is returned. * * @param name - The name of the cookie to get the value of. * @param defaultValue - An optional default value to return if the cookie is not found. */ cookie(name, defaultValue) { const cookie = this.header("cookie"); if (!cookie) { return defaultValue ?? null; } return parse(cookie, name) ?? defaultValue ?? null; } /** * Returns the value of the given parameter name from the request. If * the parameter is not found and no default value is provided, `null` is * returned. * * @param name - The name of the parameter to get the value of. * @param defaultValue - An optional default value to return if the parameter is not found. */ param(name, defaultValue) { const params = this.get("_params"); if (!params) { return defaultValue ?? null; } const value = params.get(name); if (!value) { return defaultValue ?? null; } return value; } /** * Returns the value of the first occurrence of the given query name from * the request. If the query is not found and no default value is provided, * `null` is returned. * * @param name - The name of the query to get the value of. * @param defaultValue - An optional default value to return if the query is not found. */ query(name, defaultValue) { const value = this.#url.searchParams.get(name); if (!value) { return defaultValue ?? null; } return value; } /** * Returns the values of all occurrences of the given query name from the * request. If the query is not found and no default value is provided, `null` * is returned. * * @param name - The name of the query to get the values of. * @param defaultValue - An optional default value to return if the query is not found. */ queries(name, defaultValue) { const value = this.#url.searchParams.getAll(name); if (value.length === 0) { return defaultValue ?? null; } return value; } /** * Returns the value of the given header name from the request. If the * header is not found and no default value is provided, `null` is returned. * * @param name - The name of the header to get the value of. * @param defaultValue - An optional default value to return if the header is not found. */ header(name, defaultValue) { name = name.toLowerCase(); const value = this.node.headers[name]; if (!value) { return defaultValue ?? null; } if (Array.isArray(value)) { return value.join(", "); } return value; } /** * Returns a {@link Result} with the body of the request as a {@link Uint8Array} * or `null` if the HTTP method is not `PATCH`, `POST`, or `PUT`. * * Errors that may be returned: * - {@link ContentTooLargeError} - If the body is too large. * - {@link InternalServerError} - If an error occurs while reading the body. */ async body() { const contentLength = this.header("content-length"); if (!ALLOWED_HTTP_METHODS_WITH_BODY.includes(this.method)) { return OK(null); } if (!contentLength || contentLength === "0") { return OK(null); } if (Number(contentLength) > this.#options.maxBodySize) { return ERR(new ContentTooLargeError()); } if (this.#body) { return OK(this.#body); } return new Promise( (resolve) => { let totalLength = 0; const chunks = []; const onData = (chunk) => { totalLength += chunk.length; if (totalLength > this.#options.maxBodySize) { this.node.removeListener("data", onData); this.node.removeListener("end", onEnd); this.node.removeListener("error", onError); resolve(ERR(new ContentTooLargeError())); } chunks.push(chunk); }; const onEnd = () => { this.node.removeListener("data", onData); this.node.removeListener("end", onEnd); this.node.removeListener("error", onError); this.#body = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.length, 0)); let offset = 0; for (const chunk of chunks) { this.#body.set(chunk, offset); offset += chunk.length; } resolve(OK(this.#body)); }; const onError = (err) => { this.node.removeListener("data", onData); this.node.removeListener("end", onEnd); this.node.removeListener("error", onError); resolve(ERR(new InternalServerError(err))); }; this.node.on("data", onData); this.node.on("end", onEnd); this.node.on("error", onError); } ); } /** * Returns a {@link Result} with the body of the request as a string or * `null` if the HTTP method is not `PATCH`, `POST`, or `PUT`. * * Errors that may be returned: * - {@link ContentTooLargeError} - If the body is too large. * - {@link InternalServerError} - If an error occurs while reading the body. * - {@link UnsupportedContentTypeError} - If the content type is not `text/plain`. */ async text() { const contentType = this.header("content-type")?.split(";")[0]; if (!contentType || contentType !== "text/plain") { return ERR(new UnsupportedContentTypeError()); } const result = await this.body(); if (result.isErr()) { return ERR(result.error); } if (!result.value) { return OK(null); } return OK(new TextDecoder().decode(result.value)); } /** * Returns a {@link Result} with the body of the request as a JSON object or * `null` if the HTTP method is not `PATCH`, `POST`, or `PUT`. * * Errors that may be returned: * - {@link ContentTooLargeError} - If the body is too large. * - {@link InternalServerError} - If an error occurs while reading the body. * - {@link UnsupportedContentTypeError} - If the content type is not `application/json`. * - {@link MalformedJSONError} - If the body is not valid JSON. */ async json() { const contentType = this.header("content-type")?.split(";")[0]; if (!contentType || contentType !== "application/json") { return ERR(new UnsupportedContentTypeError()); } const result = await this.body(); if (result.isErr()) { return ERR(result.error); } if (!result.value) { return OK(null); } try { return OK(JSON.parse(new TextDecoder().decode(result.value))); } catch (err) { if (err instanceof SyntaxError) { return ERR(new MalformedJSONError()); } return ERR(new InternalServerError(err)); } } }; // src/response.ts var Response = class { node; constructor(res) { this.node = res; } /** * Returns `true` if the response has been sent, otherwise `false`. */ get finished() { return this.node.writableFinished; } /** * Sends the HTTP response with the specified status code and ends the * response. If the response has already been sent, this method is a no-op * and does nothing. * * @param code - The HTTP status code to send (e.g., 200, 404, 500). */ status(code) { if (this.finished) { return; } this.node.writeHead(code).end(); } /** * Sends a 200 status code to the response and ends the response. If the * response has already been sent, this method is a no-op and does nothing. * * This method is a shorthand for `status(HTTPStatusCode.OK)`. */ ok() { this.status(HTTPStatusCode.OK); } /** * Sets a cookie on the response. If the response has already been sent, * this method does nothing. * * @param name - The name of the cookie. * @param value - The value of the cookie. * @param options - The options for the cookie. */ cookie(name, value, options) { if (this.finished) { return; } this.node.setHeader("Set-Cookie", serialise(name, value, options)); } /** * Sets the given header key and value on the response. If the response has * already been sent, this method does nothing. * * @param key - The key of the header to set. * @param value - The value of the header to set. */ header(key, value) { if (this.finished) { return; } this.node.setHeader(key, value); } /** * Sends a JSON response with the specified status code and ends the response. * If the response has already been sent, this method is a no-op and does * nothing. * * @param code - The HTTP status code to send (e.g., 200, 404, 500). * @param data - The JSON data to send in the response body. */ json(code, data) { if (this.finished) { return; } const raw = JSON.stringify(data); const headers = { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(raw).toString() }; if (this.node.req.method === "HEAD") { this.node.writeHead(code, headers).end(); return; } this.node.writeHead(code, headers).end(raw); } /** * Sends a text response with the specified status code and ends the response. * If the response has already been sent, this method is a no-op and does * nothing. * * @param code - The HTTP status code to send (e.g., 200, 404, 500). * @param data - The text data to send in the response body. */ text(code, data) { if (this.finished) { return; } const headers = { "content-type": "text/plain; charset=utf-8", "content-length": Buffer.byteLength(data).toString() }; if (this.node.req.method === "HEAD") { this.node.writeHead(code, headers).end(); return; } this.node.writeHead(code, headers).end(data); } }; // src/router.ts var Node = class _Node { type; fragment; indicies; children; name; data; constructor({ type = "static", fragment = "", indicies = "", children = [], name = null, data = null } = {}) { this.type = type; this.fragment = fragment; this.indicies = indicies; this.children = children; this.name = name; this.data = data; } /** * Creates a new child node and adds it to this node's children. * The type of child node created depends on the character provided: * - `:` creates a dynamic node * - `*` creates a catch-all node * - Any other character creates a static node * * @param char - The character to create a child node for. */ createChild(char) { const child = new _Node(); switch (char) { case ":": child.type = "dynamic"; break; case "*": child.type = "catch-all"; break; } this.indicies += char; this.children.push(child); return child; } /** * Returns the child node that matches the given character. * Looks through this node's indices to find a matching child. * * @param char - The character to search for. */ findChild(char) { for (let i = 0; i < this.indicies.length; i++) { if (this.indicies[i] === char) { return this.children[i]; } } return null; } /** * Returns a human-readable string representation of the node and its children. */ stringify() { let result = "\n"; const stack = [[this, "", true]]; while (stack.length > 0) { const item = stack.pop(); if (!item) { continue; } const [node, prefix, isLast] = item; result += `${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}`; result += `${node.fragment}, type=${node.type}, `; if (node.indicies.length > 0) { result += `indicies=[${node.indicies.split("").join(", ")}], `; } if (node.name) { result += `name=${node.name}, `; } result += `data=${node.data ? "Y" : "N"}`; result += "\n"; for (let i = node.children.length - 1; i >= 0; i--) { const child = node.children[i]; const isLastChild = i === node.children.length - 1; stack.push([child, prefix + (isLast ? " " : "\u2502 "), isLastChild]); } } return result; } }; var Router = class { #nodes = /* @__PURE__ */ new Map(); /** * Returns a human-readable string representation of the router. */ stringify() { let result = "\n"; for (const [method, node] of this.#nodes.entries()) { result += `${method}`; result += node.stringify(); } return result; } /** * Adds a route to the router. * * @param method - The HTTP method to add the route for. * @param pattern - The pattern to add the route for. * @param data - The data to associate with the route. */ addRoute(method, pattern, data) { let node = this.#nodes.get(method); if (!node) { node = new Node(); this.#nodes.set(method, node); } if (!pattern.startsWith("/")) { pattern = "/" + pattern; } while (pattern.length > 0) { if (node.fragment.length > 0) { const i = indexCommonPrefix(node.fragment, pattern); if (i < node.fragment.length) { const child = new Node({ type: node.type, fragment: node.fragment.slice(i), indicies: node.indicies, children: node.children, name: node.name, data: node.data }); node.fragment = pattern.slice(0, i); node.indicies = child.fragment[0]; node.children = [child]; node.name = null; node.data = null; } pattern = pattern.slice(i); switch (node.type) { case "dynamic": case "catch-all": pattern = pattern.slice(indexAny(pattern, "/")); break; } if (pattern.length > 0) { node = node.findChild(pattern[0]) ?? node.createChild(pattern[0]); } continue; } switch (node.type) { case "static": { const i = indexAny(pattern, ":*"); node.fragment = pattern.slice(0, i); pattern = pattern.slice(i); break; } case "dynamic": { const i = indexAny(pattern, "/"); const name = pattern.slice(1, i); if (name.length === 0) { return ERR(new Error("Dynamic parameter must have a name.")); } if (name.includes(":") || name.includes("*")) { return ERR( new Error("Only one dynamic or catch-all parameter is allowed per path segment.") ); } node.fragment = ":"; node.name = name; pattern = pattern.slice(i); break; } case "catch-all": { const i = indexAny(pattern, "/"); const name = pattern.slice(1, i); if (name.length === 0) { return ERR(new Error("Catch-all parameter must have a name.")); } if (name.length + 1 !== pattern.length) { return ERR(new Error("Catch-all parameter must be the last path segment.")); } if (name.includes(":") || name.includes("*")) { return ERR( new Error("Only one dynamic or catch-all parameter is allowed per path segment.") ); } node.fragment = "*"; node.name = name; pattern = pattern.slice(pattern.length); break; } } if (pattern.length > 0) { node = node.createChild(pattern[0]); } } node.data = data; return OK(); } /** * Returns the data and parameters for the route that matches the given * pathname. If no route is found, `null` is returned. * * @param method - The HTTP method to find the route for. * @param pathname - The pathname to find the route for. */ findRoute(method, pathname) { const node = this.#nodes.get(method); if (!node) { return null; } const params = /* @__PURE__ */ new Map(); const data = this.#lookup(node, pathname, params); if (!data) { return null; } return { data, params: params.size > 0 ? params : null }; } #lookup(node, pathname, params) { const i = indexCommonPrefix(node.fragment, pathname); if (i === pathname.length) { return node.data; } pathname = pathname.slice(i); let child = node.findChild(pathname[0]); if (child) { const data = this.#lookup(child, pathname, params); if (data) { return data; } } child = node.findChild(":"); if (child) { const i2 = indexAny(pathname, "/"); const value = pathname.slice(0, i2); params.set(child.name, value); if (i2 === pathname.length) { return child.data; } if (child.children.length === 1) { const data = this.#lookup(child.children[0], pathname.slice(i2), params); if (data) { return data; } } params.delete(child.name); } child = node.findChild("*"); if (child) { params.set(child.name, pathname); return child.data; } return null; } }; function indexCommonPrefix(a, b) { const l = Math.min(a.length, b.length); for (let i = 0; i < l; i++) { if (a[i] !== b[i]) { return i; } } return l; } function indexAny(s, chars) { let earliest = s.length; for (const char of chars) { for (let i = 0; i < s.length; i++) { if (s[i] === char && i < earliest) { earliest = i; } } } return earliest; } // src/zing.ts var DEFAULT_404_HANDLER = (_, res) => { res.text(HTTPStatusCode.NotFound, "Not Found"); }; var DEFAULT_ERROR_HANDLER = (err, _, res) => { res.header("connection", "close"); if (err instanceof ContentTooLargeError) { res.text(HTTPStatusCode.ContentTooLarge, "Content Too Large"); return; } if (err instanceof UnsupportedContentTypeError) { res.text(HTTPStatusCode.UnsupportedMediaType, "Unsupported Media Type"); return; } if (err instanceof MalformedJSONError) { res.text(HTTPStatusCode.UnprocessableContent, "Unprocessable Content"); return; } res.text(HTTPStatusCode.InternalServerError, "Internal Server Error"); }; var Zing = class { #isListening = false; #isShuttingDown = false; #activeRequestCountPerSocket = /* @__PURE__ */ new Map(); #middleware = []; #fn404Handler = DEFAULT_404_HANDLER; #fnErrorHandler = DEFAULT_ERROR_HANDLER; #options; #server; #router; constructor(options = {}) { this.#options = { ...DEFAULT_OPTIONS, ...options }; this.#server = createHTTPServer(this.#dispatch.bind(this)); this.#router = new Router(); } /** * Starts listening for incoming connections on the specified port. * * @param port - The port to listen on. Defaults to `8080`. */ async listen(port = 8080) { if (this.#isListening) { return; } this.#isListening = true; await new Promise((resolve, reject) => { this.#server.on("connection", (socket) => { this.#activeRequestCountPerSocket.set(socket, 0); socket.on("close", () => this.#activeRequestCountPerSocket.delete(socket)); }); this.#server.on("request", (req, res) => { this.#activeRequestCountPerSocket.set( req.socket, (this.#activeRequestCountPerSocket.get(req.socket) ?? 0) + 1 ); res.on("finish", () => { this.#activeRequestCountPerSocket.set( req.socket, (this.#activeRequestCountPerSocket.get(req.socket) ?? 0) - 1 ); if (this.#isShuttingDown && this.#activeRequestCountPerSocket.get(req.socket) === 0) { req.socket.destroy(); } }); }); this.#server.once("error", (err) => { reject(err); }); this.#server.listen(port, resolve); }); } /** * Gracefully shuts down the server by stopping it from accepting new * connections and closing all idle connections. After a specified timeout, * all connections will be closed. * * @param timeout - The timeout in milliseconds. Defaults to `10000`. */ async shutdown(timeout = 1e4) { if (this.#isShuttingDown) { return; } this.#isShuttingDown = true; let timeoutRef = null; try { await new Promise((resolve, reject) => { this.#server.close((err) => { if (err) { reject(err); return; } resolve(); }); for (const [socket, count] of this.#activeRequestCountPerSocket.entries()) { if (socket.readyState === "open" && count === 0) { socket.destroy(); } } timeoutRef = setTimeout(() => { this.#server.closeAllConnections(); }, timeout); }); } finally { if (timeoutRef) { clearTimeout(timeoutRef); } } } /** * Adds a route with the specified HTTP method, pattern, optional route-level * middleware and handler. * * @param method - The HTTP method to add the route for. * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. * * @throws {Error} If the given pattern is invalid. */ route(method, pattern, ...args) { let handler = args.at(-1); const middleware = args.slice(0, -1); for (let i = middleware.length - 1; i >= 0; i--) { handler = middleware[i](handler); } const result = this.#router.addRoute(method, pattern, { handler }); if (result.isErr()) { throw result.error; } } /** * Adds a `GET` route with the specified pattern, optional route-level * middleware and handler. * * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. */ get(pattern, ...args) { this.route("GET", pattern, ...args); } /** * Adds a `HEAD` route with the specified pattern, optional route-level * middleware and handler. * * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. */ head(pattern, ...args) { this.route("HEAD", pattern, ...args); } /** * Adds a `PATCH` route with the specified pattern, optional route-level * middleware and handler. * * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. */ patch(pattern, ...args) { this.route("PATCH", pattern, ...args); } /** * Adds a `POST` route with the specified pattern, optional route-level * middleware and handler. * * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. */ post(pattern, ...args) { this.route("POST", pattern, ...args); } /** * Adds a `PUT` route with the specified pattern, optional route-level * middleware and handler. * * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. */ put(pattern, ...args) { this.route("PUT", pattern, ...args); } /** * Adds a `DELETE` route with the specified pattern, optional route-level * middleware and handler. * * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. */ delete(pattern, ...args) { this.route("DELETE", pattern, ...args); } /** * Adds a `OPTIONS` route with the specified pattern, optional route-level * middleware and handler. * * @param pattern - The pattern to match the route against. * @param args - The middleware and handler to call when the route is matched. */ options(pattern, ...args) { this.route("OPTIONS", pattern, ...args); } /** * Adds an application-level middleware to be called for each incoming * request regardless of whether it matches a route or not. * * @param middleware - The middleware to be called for each request. */ use(...middleware) { this.#middleware.push(...middleware); } /** * Sets the handler to call when a route is not found. * * @param handler - The handler to call when a route is not found. */ set404Handler(handler) { this.#fn404Handler = handler; } /** * Sets the handler to call when an error occurs. * * @param handler - The handler to call when an error occurs. */ setErrorHandler(handler) { this.#fnErrorHandler = handler; } async #dispatch(nodeReq, nodeRes) { const req = new Request(nodeReq, this.#options); const res = new Response(nodeRes); try { const route = this.#router.findRoute(req.method, req.pathname); if (route) { req.set("_params", route.params); } let handler = route ? route.data.handler : this.#fn404Handler; for (let i = this.#middleware.length - 1; i >= 0; i--) { handler = this.#middleware[i](handler); } await handler(req, res); } catch (err) { try { await this.#fnErrorHandler(err, req, res); } catch (err2) { await DEFAULT_ERROR_HANDLER(err2, req, res); } } } }; export { BaseError, ContentTooLargeError, HTTPStatusCode, InternalServerError, MalformedJSONError, Request, Response, UnsupportedContentTypeError, Zing, Zing as default };