UNPKG

h3

Version:

Minimal H(TTP) framework built for high performance and portability.

1,514 lines 109 kB
import { NullProtoObj as EmptyObject, addRoute, createRouter, findRoute, removeRoute, routeToRegExp } from "rou3"; import { FastResponse, FastURL } from "srvx"; function freezeApp(app) { app.config = Object.freeze(app.config); app["~addRoute"] = () => { throw new Error("Cannot add routes after the server init."); }; } function withoutTrailingSlash(path) { if (!path || path === "/") return "/"; return path[path.length - 1] === "/" ? path.slice(0, -1) : path; } function withoutBase(input = "", base = "") { if (!base || base === "/") return input; const _base = withoutTrailingSlash(base); if (!input.startsWith(_base) || input.length > _base.length && input[_base.length] !== "/") return input; return "/" + input.slice(_base.length).replace(/^\/+/, ""); } function decodePathname(pathname) { return decodeURI(pathname.includes("%25") ? pathname.replace(/%25/g, "%2525") : pathname); } const kEventNS = "h3.internal.event."; const kEventRes = /* @__PURE__ */ Symbol.for(`${kEventNS}res`); const kEventResHeaders = /* @__PURE__ */ Symbol.for(`${kEventNS}res.headers`); const kEventResErrHeaders = /* @__PURE__ */ Symbol.for(`${kEventNS}res.err.headers`); const kMalformedURL = /* @__PURE__ */ Symbol.for(`${kEventNS}malformed`); var H3Event = class { app; req; url; context; static __is_event__ = true; constructor(req, context, app) { this.context = context || req.context || new EmptyObject(); this.req = req; this.app = app; const _url = req._url; let url = _url && _url instanceof URL ? _url : new FastURL(req.url); if (url.pathname.includes("%")) try { const pathname = decodePathname(url.pathname); if (pathname !== url.pathname) url = new FastURL(`${url.protocol}//${url.host}${pathname}${url.search}`); } catch { this[kMalformedURL] = true; } this.url = url; } get res() { return this[kEventRes] ||= new H3EventResponse(); } get runtime() { return this.req.runtime; } waitUntil(promise) { this.req.waitUntil?.(promise); } toString() { return `[${this.req.method}] ${this.req.url}`; } toJSON() { return this.toString(); } get node() { return this.req.runtime?.node; } get headers() { return this.req.headers; } get path() { return this.url.pathname + this.url.search; } get method() { return this.req.method; } }; var H3EventResponse = class { status; statusText; get headers() { return this[kEventResHeaders] ||= new Headers(); } get errHeaders() { return this[kEventResErrHeaders] ||= new Headers(); } }; const DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g; function sanitizeStatusMessage(statusMessage = "") { return statusMessage.replace(DISALLOWED_STATUS_CHARS, ""); } function sanitizeStatusCode(statusCode, defaultStatusCode = 200) { if (!statusCode) return defaultStatusCode; if (typeof statusCode === "string") statusCode = +statusCode; if (Number.isNaN(statusCode) || statusCode < 100 || statusCode > 599) return defaultStatusCode; return statusCode; } var HTTPError = class HTTPError extends Error { get name() { return "HTTPError"; } status; statusText; headers; cause; data; body; unhandled; static isError(input) { return input instanceof Error && input?.name === "HTTPError"; } static status(status, statusText, details) { return new HTTPError({ ...details, statusText, status }); } constructor(arg1, arg2) { let messageInput; let details; if (typeof arg1 === "string") { messageInput = arg1; details = arg2; } else details = arg1; const status = sanitizeStatusCode(details?.status || details?.statusCode || (details?.cause)?.status || (details?.cause)?.statusCode, 500); const statusText = sanitizeStatusMessage(details?.statusText || details?.statusMessage || (details?.cause)?.statusText || (details?.cause)?.statusMessage); const message = messageInput || details?.message || (details?.cause)?.message || details?.statusText || details?.statusMessage || [ "HTTPError", status, statusText ].filter(Boolean).join(" "); super(message, { cause: details }); this.cause = details; this.status = status; this.statusText = statusText || void 0; const rawHeaders = details?.headers || (details?.cause)?.headers; this.headers = rawHeaders ? new Headers(rawHeaders) : void 0; this.unhandled = details?.unhandled ?? (details?.cause)?.unhandled ?? void 0; this.data = details?.data; this.body = details?.body; } get statusCode() { return this.status; } get statusMessage() { return this.statusText; } toJSON() { const unhandled = this.unhandled; return { status: this.status, statusText: this.statusText, unhandled, message: unhandled ? "HTTPError" : this.message, data: unhandled ? void 0 : this.data, ...unhandled ? void 0 : this.body }; } }; function hasProp(obj, prop) { try { return prop in obj; } catch { return false; } } function isJSONSerializable(value, _type) { if (value === null || value === void 0) return true; if (_type !== "object") return _type === "boolean" || _type === "number" || _type === "string"; if (typeof value.toJSON === "function") return true; if (Array.isArray(value)) return true; if (typeof value.pipe === "function" || typeof value.pipeTo === "function") return false; if (value instanceof EmptyObject) return true; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } const kNotFound = /* @__PURE__ */ Symbol.for("h3.notFound"); const kHandled = /* @__PURE__ */ Symbol.for("h3.handled"); function toResponse(val, event, config = {}) { if (typeof val?.then === "function") return val.then((resolvedVal) => toResponse(resolvedVal, event, config), (r) => toResponse(typeof r === "number" ? new HTTPError({ status: r }) : r, event, config)); const response = prepareResponse(val, event, config); if (typeof response?.then === "function") return toResponse(response, event, config); const { onResponse } = config; return onResponse ? Promise.resolve(onResponse(response, event)).then(() => response) : response; } var HTTPResponse = class { #headers; #init; body; constructor(body, init) { this.body = body; this.#init = init; } get status() { return this.#init?.status || 200; } get statusText() { return this.#init?.statusText || "OK"; } get headers() { return this.#headers ||= new Headers(this.#init?.headers); } }; function prepareResponse(val, event, config, nested) { if (val === kHandled) return new FastResponse(null); if (val === kNotFound) val = new HTTPError({ status: 404, message: `Cannot find any route matching [${event.req.method}] ${event.url}` }); if (val && val instanceof Error) { const isHTTPError = HTTPError.isError(val); const error = isHTTPError ? val : new HTTPError(val); if (!isHTTPError) { error.unhandled = true; if (val?.stack) error.stack = val.stack; } if (error.unhandled && !config.silent) console.error(error); const { onError } = config; const errHeaders = event[kEventRes]?.[kEventResErrHeaders]; return onError && !nested ? Promise.resolve(onError(error, event)).catch((error) => error).then((newVal) => prepareResponse(newVal ?? val, event, config, true)) : errorResponse(error, config.debug, errHeaders); } const preparedRes = event[kEventRes]; const preparedHeaders = preparedRes?.[kEventResHeaders]; event[kEventRes] = void 0; if (!(val instanceof Response)) { const res = prepareResponseBody(val, event, config); const status = res.status || preparedRes?.status; return new FastResponse(nullBody(event.req.method, status) ? null : res.body, { status, statusText: res.statusText || preparedRes?.statusText, headers: res.headers && preparedHeaders ? mergeHeaders$1(res.headers, preparedHeaders) : res.headers || preparedHeaders }); } if (!preparedHeaders || nested || !val.ok) { if (event.req.method === "HEAD" && val.body !== null) return new FastResponse(null, { status: val.status, statusText: val.statusText, headers: val.headers }); return val; } try { mergeHeaders$1(val.headers, preparedHeaders, val.headers); return val; } catch { return new FastResponse(nullBody(event.req.method, val.status) ? null : val.body, { status: val.status, statusText: val.statusText, headers: mergeHeaders$1(val.headers, preparedHeaders) }); } } function mergeHeaders$1(base, overrides, target = new Headers(base)) { for (const [name, value] of overrides) if (name === "set-cookie") target.append(name, value); else target.set(name, value); return target; } const frozen = (name) => (...args) => { throw new Error(`Headers are frozen (${name} ${args.join(", ")})`); }; var FrozenHeaders = class extends Headers { set = frozen("set"); append = frozen("append"); delete = frozen("delete"); }; const emptyHeaders = /* @__PURE__ */ new FrozenHeaders({ "content-length": "0" }); const jsonHeaders = /* @__PURE__ */ new FrozenHeaders({ "content-type": "application/json;charset=UTF-8" }); function prepareResponseBody(val, event, config) { if (val === null || val === void 0) return { body: "", headers: emptyHeaders }; const valType = typeof val; if (valType === "string") return { body: val }; if (val instanceof Uint8Array) { event.res.headers.set("content-length", val.byteLength.toString()); return { body: val }; } if (val instanceof HTTPResponse || val?.constructor?.name === "HTTPResponse") return val; if (isJSONSerializable(val, valType)) return { body: JSON.stringify(val, void 0, config.debug ? 2 : void 0), headers: jsonHeaders }; if (valType === "bigint") return { body: val.toString(), headers: jsonHeaders }; if (val instanceof Blob) { const headers = new Headers({ "content-type": val.type, "content-length": val.size.toString() }); let filename = val.name; if (filename) { filename = encodeURIComponent(filename); headers.set("content-disposition", `filename="${filename}"; filename*=UTF-8''${filename}`); } return { body: val.stream(), headers }; } if (valType === "symbol") return { body: val.toString() }; if (valType === "function") return { body: `${val.name}()` }; return { body: val }; } function nullBody(method, status) { return method === "HEAD" || status === 100 || status === 101 || status === 102 || status === 204 || status === 205 || status === 304; } function errorResponse(error, debug, errHeaders) { let headers = error.headers ? mergeHeaders$1(jsonHeaders, error.headers) : new Headers(jsonHeaders); if (errHeaders) headers = mergeHeaders$1(headers, errHeaders); return new FastResponse(JSON.stringify({ ...error.toJSON(), stack: debug && error.stack ? error.stack.split("\n").map((l) => l.trim()) : void 0 }, void 0, debug ? 2 : void 0), { status: error.status, statusText: error.statusText, headers }); } function defineMiddleware(input) { return input; } function normalizeMiddleware(input, opts = {}) { const matcher = createMatcher(opts); if (!matcher && (input.length > 1 || input.constructor?.name === "AsyncFunction")) return input; return (event, next) => { if (matcher && !matcher(event)) return next(); const res = input(event, next); return res === void 0 || res === kNotFound ? next() : res; }; } function createMatcher(opts) { if (!opts.route && !opts.method && !opts.match) return; const routeMatcher = opts.route ? routeToRegExp(opts.route) : void 0; const method = opts.method?.toUpperCase(); return function _middlewareMatcher(event) { if (method && event.req.method !== method) { if (!(method === "GET" && event.req.method === "HEAD")) return false; } if (opts.match && !opts.match(event)) return false; if (!routeMatcher) return true; const match = event.url.pathname.match(routeMatcher); if (!match) return false; if (match.groups) event.context.middlewareParams = { ...event.context.middlewareParams, ...match.groups }; return true; }; } function callMiddleware(event, middleware, handler, index = 0) { if (index === middleware.length) return handler(event); const fn = middleware[index]; let nextCalled; let nextResult; const next = () => { if (nextCalled) return nextResult; nextCalled = true; nextResult = callMiddleware(event, middleware, handler, index + 1); return nextResult; }; const ret = fn(event, next); return isUnhandledResponse(ret) ? next() : typeof ret?.then === "function" ? ret.then((resolved) => isUnhandledResponse(resolved) ? next() : resolved) : ret; } function isUnhandledResponse(val) { return val === void 0 || val === kNotFound; } function toMiddleware(input) { let h = input.handler || input; let isFunction = typeof h === "function"; if (!isFunction && typeof input?.fetch === "function") { isFunction = true; h = function _fetchHandler(event) { return input.fetch(event.req); }; } if (!isFunction) return function noopMiddleware(event, next) { return next(); }; if (h.length === 2) return h; return function _middlewareHandler(event, next) { const res = h(event); return typeof res?.then === "function" ? res.then((r) => { return is404(r) ? next() : r; }) : is404(res) ? next() : res; }; } function is404(val) { return isUnhandledResponse(val) || val?.status === 404 && val instanceof Response; } const plusRegex = /\+/g; function parseQuery(input) { const params = new EmptyObject(); if (!input || input === "?") return params; const inputLength = input.length; let key = ""; let value = ""; let startingIndex = -1; let equalityIndex = -1; let shouldDecodeKey = false; let shouldDecodeValue = false; let keyHasPlus = false; let valueHasPlus = false; let hasBothKeyValuePair = false; let c = 0; for (let i = 0; i < inputLength + 1; i++) { c = i === inputLength ? 38 : input.charCodeAt(i); switch (c) { case 38: hasBothKeyValuePair = equalityIndex > startingIndex; if (!hasBothKeyValuePair) equalityIndex = i; key = input.slice(startingIndex + 1, equalityIndex); if (hasBothKeyValuePair || key.length > 0) { if (keyHasPlus) key = key.replace(plusRegex, " "); if (shouldDecodeKey) try { key = decodeURIComponent(key); } catch {} if (hasBothKeyValuePair) { value = input.slice(equalityIndex + 1, i); if (valueHasPlus) value = value.replace(plusRegex, " "); if (shouldDecodeValue) try { value = decodeURIComponent(value); } catch {} } const currentValue = params[key]; if (currentValue === void 0) params[key] = value; else if (Array.isArray(currentValue)) currentValue.push(value); else params[key] = [currentValue, value]; } value = ""; startingIndex = i; equalityIndex = i; shouldDecodeKey = false; shouldDecodeValue = false; keyHasPlus = false; valueHasPlus = false; break; case 61: if (equalityIndex <= startingIndex) equalityIndex = i; else shouldDecodeValue = true; break; case 43: if (equalityIndex > startingIndex) valueHasPlus = true; else keyHasPlus = true; break; case 37: if (equalityIndex > startingIndex) shouldDecodeValue = true; else shouldDecodeKey = true; break; } } return params; } const VALIDATION_FAILED = "Validation failed"; async function validateData(data, fn, options) { if ("~standard" in fn) { const result = await fn["~standard"].validate(data); if (result.issues) throw createValidationError(options?.onError?.(result) || { message: VALIDATION_FAILED, issues: result.issues }); return result.value; } try { const res = await fn(data); if (res === false) throw createValidationError(options?.onError?.({ issues: [{ message: VALIDATION_FAILED }] }) || { message: VALIDATION_FAILED }); if (res === true) return data; return res ?? data; } catch (error) { throw createValidationError(error); } } const reqBodyKeys = /* @__PURE__ */ new Set([ "body", "text", "formData", "arrayBuffer" ]); function validatedRequest(req, validate) { if (validate.headers) { const validatedheaders = syncValidate("headers", Object.fromEntries(req.headers.entries()), validate.headers, validate.onError); for (const [key, value] of Object.entries(validatedheaders)) req.headers.set(key, value); } if (!validate.body) return req; return new Proxy(req, { get(_target, prop) { if (prop === "json") return function _validatedJson() { return req.json().then((data) => validate.body["~standard"].validate(data)).then((result) => { if (result.issues) throw createValidationError(validate.onError?.({ _source: "body", ...result }) || { message: VALIDATION_FAILED, issues: result.issues }); return result.value; }); }; else if (reqBodyKeys.has(prop)) throw new TypeError(`Cannot access .${prop} on request with JSON validation enabled. Use .json() instead.`); return Reflect.get(req, prop); } }); } function validatedURL(url, validate) { if (!validate.query) return url; const validatedQuery = syncValidate("query", Object.fromEntries(url.searchParams.entries()), validate.query, validate.onError); for (const [key, value] of Object.entries(validatedQuery)) url.searchParams.set(key, value); return url; } function syncValidate(source, data, fn, onError) { const result = fn["~standard"].validate(data); if (result instanceof Promise) throw new TypeError(`Asynchronous validation is not supported for ${source}`); if (result.issues) throw createValidationError(onError?.({ _source: source, ...result }) || { message: VALIDATION_FAILED, issues: result.issues }); return result.value; } function createValidationError(cause) { return HTTPError.isError(cause) ? cause : new HTTPError({ cause, status: cause?.status || 400, statusText: cause?.statusText || VALIDATION_FAILED, message: cause?.message || VALIDATION_FAILED, data: { issues: cause?.issues, message: cause instanceof Error ? VALIDATION_FAILED : cause?.message || VALIDATION_FAILED } }); } function isEvent(input) { return input instanceof H3Event || input?.constructor?.__is_event__; } function isHTTPEvent(input) { return input?.req instanceof Request; } function getEventContext(event) { if (event.context) return event.context; event.req.context ??= {}; return event.req.context; } function mockEvent(_request, options) { let request; if (options?.body && !options.duplex) options.duplex = "half"; if (typeof _request === "string") { let url = _request; if (url[0] === "/") url = `http://localhost${url}`; request = new Request(url, options); } else if (options || _request instanceof URL) request = new Request(_request, options); else request = _request; return new H3Event(request); } function requestWithURL(req, url) { const cache = { url, _url: void 0 }; return new Proxy(req, { get(target, prop) { if (prop in cache) return cache[prop]; const value = Reflect.get(target, prop); cache[prop] = typeof value === "function" ? value.bind(target) : value; return cache[prop]; } }); } function requestWithBaseURL(req, base) { const url = new URL(req.url); let pathname; try { pathname = decodePathname(url.pathname); } catch { pathname = url.pathname; } url.pathname = pathname.slice(base.length) || "/"; return requestWithURL(req, url.href); } function toRequest(input, options) { if (typeof input === "string") { let url = input; if (url[0] === "/") { const headers = options?.headers ? new Headers(options.headers) : void 0; const host = headers?.get("host") || "localhost"; url = `${(headers?.get("x-forwarded-proto") || "").split(",")[0].trim() === "https" ? "https" : "http"}://${host}${url}`; } return new Request(url, options); } else if (options || input instanceof URL) return new Request(input, options); return input; } function getQuery(event) { return parseQuery((event.url || new URL(event.req.url)).search.slice(1)); } function getValidatedQuery(event, validate, options) { return validateData(getQuery(event), validate, options); } function getRouterParams(event, opts = {}) { let params = getEventContext(event).params || {}; if (opts.decode) { params = { ...params }; for (const key in params) params[key] = decodeRouterParam(params[key]); } return params; } const ENCODED_SEP_RE_G$1 = /%(?:25)*(?:2f|5c)/gi; function decodeRouterParam(value) { if (!value.includes("%")) return value; let result = ""; let lastIndex = 0; ENCODED_SEP_RE_G$1.lastIndex = 0; for (let m; m = ENCODED_SEP_RE_G$1.exec(value);) { result += decodeURIComponent(value.slice(lastIndex, m.index)) + m[0]; lastIndex = m.index + m[0].length; } return result + decodeURIComponent(value.slice(lastIndex)); } function getValidatedRouterParams(event, validate, options = {}) { const { decode, ...opts } = options; return validateData(getRouterParams(event, { decode }), validate, opts); } function getRouterParam(event, name, opts = {}) { return getRouterParams(event, opts)[name]; } function isMethod(event, expected, allowHead) { if (allowHead && event.req.method === "HEAD") return true; if (typeof expected === "string") { if (event.req.method === expected) return true; } else if (expected.includes(event.req.method)) return true; return false; } function assertMethod(event, expected, allowHead) { if (!isMethod(event, expected, allowHead)) { const allowed = Array.isArray(expected) ? expected : [expected]; throw new HTTPError({ status: 405, headers: { Allow: allowHead ? [...allowed, "HEAD"].join(", ") : allowed.join(", ") } }); } } function getRequestHost(event, opts = {}) { if (opts.xForwardedHost) { const xForwardedHost = (event.req.headers.get("x-forwarded-host") || "").split(",").shift()?.trim(); if (xForwardedHost) return xForwardedHost; } return event.req.headers.get("host") || ""; } function getRequestProtocol(event, opts = {}) { if (opts.xForwardedProto !== false) { const forwardedProto = (event.req.headers.get("x-forwarded-proto") || "").split(",")[0].trim(); if (forwardedProto === "https") return "https"; if (forwardedProto === "http") return "http"; } return (event.url || new URL(event.req.url)).protocol.slice(0, -1); } function getRequestURL(event, opts = {}) { const url = new URL(event.url || event.req.url); url.protocol = getRequestProtocol(event, opts); if (opts.xForwardedHost) { const host = getRequestHost(event, opts); if (host) { url.host = host; if (!/:\d+$/.test(host)) url.port = ""; } } return url; } function getRequestIP(event, opts = {}) { if (opts.xForwardedFor) { const _header = event.req.headers.get("x-forwarded-for"); if (_header) { const xForwardedFor = _header.split(",")[0].trim(); if (xForwardedFor) return xForwardedFor; } } return event.req.context?.clientAddress || event.req.ip || void 0; } function defineHandler(input) { if (typeof input === "function") return handlerWithFetch(input); const handler = input.handler || (input.fetch ? function _fetchHandler(event) { return input.fetch(event.req); } : NoHandler); return Object.assign(handlerWithFetch(input.middleware?.length ? function _handlerMiddleware(event) { return callMiddleware(event, input.middleware, handler); } : handler), input); } function defineValidatedHandler(def) { if (!def.validate) return defineHandler(def); return defineHandler({ ...def, handler: function _validatedHandler(event) { event.req = validatedRequest(event.req, def.validate); event.url = validatedURL(event.url, def.validate); return def.handler(event); } }); } function handlerWithFetch(handler) { if ("fetch" in handler) return handler; return Object.assign(handler, { fetch: (req) => { if (typeof req === "string") req = new URL(req, "http://_"); if (req instanceof URL) req = new Request(req); const event = new H3Event(req); try { return Promise.resolve(toResponse(handler(event), event)); } catch (error) { return Promise.resolve(toResponse(error, event)); } } }); } function dynamicEventHandler(initial) { let current = toEventHandler(initial); return Object.assign(defineHandler(function _dynamicEventHandler(event) { return current?.(event); }), { set: (handler) => { current = toEventHandler(handler); } }); } function defineLazyEventHandler(loader) { let handler; let promise; return defineHandler(function lazyHandler(event) { return handler ? handler(event) : (promise ??= Promise.resolve(loader()).then(function resolveLazyHandler(r) { handler = toEventHandler(r) || toEventHandler(r.default); if (typeof handler !== "function") throw new TypeError("Invalid lazy handler", { cause: { resolved: r } }); return handler; })).then((r) => r(event)); }); } function toEventHandler(handler) { if (typeof handler === "function") return handler; if (typeof handler?.handler === "function" && handler.constructor?.["~h3"]) return handler.handler; if (typeof handler?.fetch === "function") return function _fetchHandler(event) { return handler.fetch(event.req); }; } const NoHandler = () => kNotFound; var H3Core = class { static "~h3" = true; config; "~middleware"; "~routes" = []; constructor(config = {}) { this["~middleware"] = []; this.config = config; this.fetch = this.fetch.bind(this); this.handler = this.handler.bind(this); } fetch(request) { return this["~request"](request); } handler(event) { const route = this["~findRoute"](event); if (route) { event.context.params = route.params; event.context.matchedRoute = route.data; } const routeHandler = route?.data.handler || NoHandler; const middleware = this["~getMiddleware"](event, route); return middleware.length > 0 ? callMiddleware(event, middleware, routeHandler) : routeHandler(event); } "~request"(request, context) { const event = new H3Event(request, context, this); let handlerRes; try { if (event[kMalformedURL] && !this.config.allowMalformedURL) throw new HTTPError({ status: 400, message: "Bad Request" }); if (this.config.onRequest) { const hookRes = this.config.onRequest(event); handlerRes = typeof hookRes?.then === "function" ? hookRes.then(() => this.handler(event)) : this.handler(event); } else handlerRes = this.handler(event); } catch (error) { handlerRes = Promise.reject(error); } return toResponse(handlerRes, event, this.config); } "~findRoute"(_event) {} "~addRoute"(_route) { this["~routes"].push(_route); } "~getMiddleware"(_event, route) { const routeMiddleware = route?.data.middleware; const globalMiddleware = this["~middleware"]; return routeMiddleware ? [...globalMiddleware, ...routeMiddleware] : globalMiddleware; } }; const H3 = /* @__PURE__ */ (() => { class H3 extends H3Core { "~rou3"; constructor(config = {}) { super(config); this["~rou3"] = createRouter(); this.request = this.request.bind(this); config.plugins?.forEach((plugin) => plugin(this)); } register(plugin) { plugin(this); return this; } request(_req, _init, context) { return this["~request"](toRequest(_req, _init), context); } mount(base, input) { if ("handler" in input) { if (input["~middleware"].length > 0) this["~middleware"].push((event, next) => { const originalPathname = event.url.pathname; if (!originalPathname.startsWith(base) || originalPathname.length > base.length && originalPathname[base.length] !== "/") return next(); event.url.pathname = event.url.pathname.slice(base.length) || "/"; const restore = () => { event.url.pathname = originalPathname; }; try { const result = callMiddleware(event, input["~middleware"], () => { restore(); return next(); }); if (typeof result?.then === "function") return Promise.resolve(result).finally(restore); restore(); return result; } catch (err) { restore(); throw err; } }); for (const r of input["~routes"]) this["~addRoute"]({ ...r, route: base + r.route }); } else { const fetchHandler = "fetch" in input ? input.fetch : input; this.all(`${base}/**`, function _mountedMiddleware(event) { return fetchHandler(requestWithBaseURL(event.req, base)); }); } return this; } on(method, route, handler, opts) { const _method = (method || "").toUpperCase(); route = new URL(route, "http://_").pathname; this["~addRoute"]({ method: _method, route, handler: toEventHandler(handler), middleware: opts?.middleware, meta: { ...handler.meta, ...opts?.meta } }); return this; } all(route, handler, opts) { return this.on("", route, handler, opts); } "~findRoute"(_event) { const match = findRoute(this["~rou3"], _event.req.method, _event.url.pathname); if (match === void 0 && _event.req.method === "HEAD") return findRoute(this["~rou3"], "GET", _event.url.pathname); return match; } "~addRoute"(_route) { addRoute(this["~rou3"], _route.method, _route.route, _route); super["~addRoute"](_route); } use(arg1, arg2, arg3) { let route; let fn; let opts; if (typeof arg1 === "string") { route = arg1; fn = arg2; opts = arg3; } else { fn = arg1; opts = arg2; } if (typeof fn !== "function" && "handler" in fn) return this.mount(route || "", fn); this["~middleware"].push(normalizeMiddleware(fn, { ...opts, route })); return this; } } for (const method of [ "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE", "QUERY" ]) H3Core.prototype[method.toLowerCase()] = function(route, handler, opts) { return this.on(method, route, handler, opts); }; return H3; })(); function definePlugin(def) { return ((opts) => (h3) => def(h3, opts)); } function toWebHandler(app) { return (request, context) => { return Promise.resolve(app.request(request, void 0, context || request.context)); }; } function fromWebHandler(handler) { return function _webHandler(event) { return handler(event.req, event.context); }; } function fromNodeHandler(handler) { if (typeof handler !== "function") throw new TypeError(`Invalid handler. It should be a function: ${handler}`); return function _nodeHandler(event) { const node = event.runtime?.node; if (!node?.res) throw new Error("[h3] Executing Node.js middleware is not supported in this server!"); const url = event.url.pathname + event.url.search; if (node.req.url === url) return callNodeHandler(handler, node.req, node.res); const originalUrl = node.req.url; node.req.url = url; return callNodeHandler(handler, node.req, node.res).finally(() => { node.req.url = originalUrl; }); }; } function defineNodeHandler(handler) { return handler; } function defineNodeMiddleware(handler) { return handler; } function callNodeHandler(handler, req, res) { const isMiddleware = handler.length > 2; return new Promise((resolve, reject) => { res.once("close", () => resolve(kHandled)); res.once("finish", () => resolve(kHandled)); res.once("error", (error) => reject(error)); res.once("pipe", (stream) => { resolve(new Promise((resolve, reject) => { stream.once("close", () => resolve(kHandled)); stream.once("error", (error) => { console.error("[h3] Stream error in Node.js handler", { cause: error }); reject(kHandled); }); })); }); try { if (isMiddleware) Promise.resolve(handler(req, res, (error) => error ? reject(new HTTPError({ cause: error, unhandled: true })) : resolve(void 0))).catch((error) => reject(new HTTPError({ cause: error, unhandled: true }))); else return Promise.resolve(handler(req, res)).then(() => resolve(kHandled)).catch((error) => reject(new HTTPError({ cause: error, unhandled: true }))); } catch (error) { reject(new HTTPError({ cause: error, unhandled: true })); } }); } function defineRoute(def) { const handler = defineValidatedHandler(def); return (h3) => { h3.on(def.method, def.route, handler); }; } function removeRoute$1(app, method, route) { const _method = method ? method.toUpperCase() : void 0; route = new URL(route, "http://_").pathname; removeRoute(app["~rou3"], _method || "", route); const idx = app["~routes"].findIndex((r) => r.route === route && (_method == null || r.method === _method)); if (idx !== -1) app["~routes"].splice(idx, 1); } const textEncoder = /* @__PURE__ */ new TextEncoder(); const textDecoder = /* @__PURE__ */ new TextDecoder(); const base64Code = [ 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 45, 95 ]; function base64Encode(data) { const buff = validateBinaryLike(data); if (globalThis.Buffer) return globalThis.Buffer.from(buff).toString("base64url"); const bytes = []; let i; const len = buff.length; for (i = 2; i < len; i += 3) bytes.push(base64Code[buff[i - 2] >> 2], base64Code[(buff[i - 2] & 3) << 4 | buff[i - 1] >> 4], base64Code[(buff[i - 1] & 15) << 2 | buff[i] >> 6], base64Code[buff[i] & 63]); if (i === len + 1) bytes.push(base64Code[buff[i - 2] >> 2], base64Code[(buff[i - 2] & 3) << 4]); if (i === len) bytes.push(base64Code[buff[i - 2] >> 2], base64Code[(buff[i - 2] & 3) << 4 | buff[i - 1] >> 4], base64Code[(buff[i - 1] & 15) << 2]); return String.fromCharCode(...bytes); } function base64Decode(b64Url) { if (globalThis.Buffer) return new Uint8Array(globalThis.Buffer.from(b64Url, "base64url")); const b64 = b64Url.replace(/-/g, "+").replace(/_/g, "/"); const binString = atob(b64); const size = binString.length; const bytes = new Uint8Array(size); for (let i = 0; i < size; i++) bytes[i] = binString.charCodeAt(i); return bytes; } function validateBinaryLike(source) { if (typeof source === "string") return textEncoder.encode(source); else if (source instanceof Uint8Array) return source; else if (source instanceof ArrayBuffer) return new Uint8Array(source); throw new TypeError(`The input must be a Uint8Array, a string, or an ArrayBuffer.`); } function serializeIterableValue(value) { switch (typeof value) { case "string": return textEncoder.encode(value); case "boolean": case "number": case "bigint": case "symbol": return textEncoder.encode(value.toString()); case "object": if (value instanceof Uint8Array) return value; return textEncoder.encode(JSON.stringify(value)); } return /* @__PURE__ */ new Uint8Array(); } function coerceIterable(iterable) { if (typeof iterable === "function") iterable = iterable(); if (Symbol.iterator in iterable) return iterable[Symbol.iterator](); if (Symbol.asyncIterator in iterable) return iterable[Symbol.asyncIterator](); return iterable; } function noContent(status = 204) { return new HTTPResponse(null, { status, statusText: "No Content" }); } function redirect(location, status = 302, statusText) { return new HTTPResponse(`<html><head><meta http-equiv="refresh" content="0; url=${location.replace(/[&"<>]/g, (c) => ({ "&": "&amp;", "\"": "&quot;", "<": "&lt;", ">": "&gt;" })[c])}" /></head></html>`, { status, statusText: statusText || (status === 301 ? "Moved Permanently" : "Found"), headers: { "content-type": "text/html; charset=utf-8", location } }); } function redirectBack(event, opts = {}) { const referer = event.req.headers.get("referer"); let location = opts.fallback ?? "/"; if (referer && URL.canParse(referer)) { const refererURL = new URL(referer); if (refererURL.origin === event.url.origin) { let pathname = refererURL.pathname; if (pathname.startsWith("//")) pathname = "/" + pathname.replace(/^\/+/, ""); location = pathname + (opts.allowQuery ? refererURL.search : ""); } } return redirect(location, opts.status); } function writeEarlyHints(event, hints) { const linkValues = []; for (const [name, value] of Object.entries(hints)) if (name.toLowerCase() === "link") { for (const v of Array.isArray(value) ? value : [value]) if (v) linkValues.push(v); } if (event.runtime?.node?.res?.writeEarlyHints) { if (linkValues.length === 0) return Promise.resolve(); const normalizedHints = { link: linkValues }; for (const [name, value] of Object.entries(hints)) if (name.toLowerCase() !== "link") normalizedHints[name] = value; return new Promise((resolve) => { event.runtime?.node?.res?.writeEarlyHints(normalizedHints, () => resolve()); }); } for (const v of linkValues) event.res.headers.append("link", v); } function iterable(iterable, options) { const serializer = options?.serializer ?? serializeIterableValue; const iterator = coerceIterable(iterable); return new HTTPResponse(new ReadableStream({ async pull(controller) { const { value, done } = await iterator.next(); if (value !== void 0) { const chunk = serializer(value); if (chunk !== void 0) controller.enqueue(chunk); } if (done) controller.close(); }, cancel() { iterator.return?.(); } })); } function html(first, ...values) { return new HTTPResponse(typeof first === "string" ? first : first.reduce((out, str, i) => out + str + (values[i] ?? ""), ""), { headers: { "content-type": "text/html; charset=utf-8" } }); } function appendAcceptQuery(event, mediaTypes) { const list = Array.isArray(mediaTypes) ? mediaTypes : [mediaTypes]; if (list.length === 0) return; const value = list.map(serializeMediaType).join(", "); event.res.headers.append("accept-query", value); } function requireContentType(event, acceptedTypes) { const header = event.req.headers.get("content-type"); if (!header) throw new HTTPError({ status: 400, statusText: "Bad Request", message: "Content-Type header is required" }); const mediaType = header.split(";")[0].trim().toLowerCase(); const slash = mediaType.indexOf("/"); if (slash <= 0 || slash === mediaType.length - 1) throw new HTTPError({ status: 422, statusText: "Unprocessable Content", message: "Malformed Content-Type header" }); const accepted = Array.isArray(acceptedTypes) ? acceptedTypes : [acceptedTypes]; if (accepted.some((type) => mediaTypeMatches(mediaType, type.split(";")[0].trim().toLowerCase()))) return mediaType; throw new HTTPError({ status: 415, statusText: "Unsupported Media Type", message: `Unsupported Content-Type: ${mediaType}. Expected one of: ${accepted.join(", ")}` }); } const SF_TOKEN_RE = /^[A-Za-z*][\w!#$%&'*+.^`|~:/-]*$/; const SF_KEY_RE = /^[a-z*][a-z0-9_.*-]*$/; function serializeMediaType(mediaType) { const parts = splitOutsideQuotes(mediaType, ";"); const base = parts[0].trim(); if (!SF_TOKEN_RE.test(base)) throw new TypeError(`Invalid media type: ${JSON.stringify(mediaType)}`); let result = base; for (let i = 1; i < parts.length; i++) { const param = parts[i].trim(); if (!param) continue; const eq = param.indexOf("="); const key = (eq === -1 ? param : param.slice(0, eq)).trim().toLowerCase(); if (!SF_KEY_RE.test(key)) throw new TypeError(`Invalid media type parameter: ${JSON.stringify(param)}`); result += eq === -1 ? `;${key}` : `;${key}="${escapeQuotes(unquote(param.slice(eq + 1).trim()))}"`; } return result; } function mediaTypeMatches(mediaType, accepted) { if (accepted === "*/*" || accepted === "*") return true; if (accepted === mediaType) return true; if (accepted.endsWith("/*")) return mediaType.startsWith(accepted.slice(0, -1)); return false; } function splitOutsideQuotes(input, sep) { const parts = []; let current = ""; let inQuotes = false; for (let i = 0; i < input.length; i++) { const ch = input[i]; if (inQuotes) { current += ch; if (ch === "\\" && i + 1 < input.length) current += input[++i]; else if (ch === "\"") inQuotes = false; } else if (ch === "\"") { inQuotes = true; current += ch; } else if (ch === sep) { parts.push(current); current = ""; } else current += ch; } parts.push(current); return parts; } function escapeQuotes(value) { return value.replace(/[\\"]/g, "\\$&"); } function unquote(value) { if (value.length >= 2 && value[0] === "\"" && value.endsWith("\"")) return value.slice(1, -1).replace(/\\(.)/g, "$1"); return value; } function parseURLEncodedBody(body) { return collectEntries(new URLSearchParams(body).entries()); } function parseFormData(form) { return collectEntries(form.entries()); } function collectEntries(entries) { const parsed = new EmptyObject(); for (const [key, value] of entries) if (hasProp(parsed, key)) { if (!Array.isArray(parsed[key])) parsed[key] = [parsed[key]]; parsed[key].push(value); } else parsed[key] = value; return parsed; } async function readBody(event, options) { const contentType = event.req.headers.get("content-type") || ""; const type = options?.type; if (type === "formData") { let form; try { form = await event.req.formData(); } catch { throw new HTTPError({ status: 400, statusText: "Bad Request", message: "Invalid form data body" }); } return parseFormData(form); } const text = await event.req.text(); if (type === "text") return text; if (!text) return; if (type === "urlencoded" || !type && contentType.startsWith("application/x-www-form-urlencoded")) return parseURLEncodedBody(text); try { return JSON.parse(text); } catch { throw new HTTPError({ status: 400, statusText: "Bad Request", message: "Invalid JSON body" }); } } async function readValidatedBody(event, validate, options) { return validateData(await readBody(event), validate, options); } async function assertBodySize(event, limit) { if (!await isBodySizeWithin(event, limit)) throw new HTTPError({ status: 413, statusText: "Request Entity Too Large", message: `Request body size exceeds the limit of ${limit} bytes` }); } async function isBodySizeWithin(event, limit) { const req = event.req; if (req.body === null) return true; const contentLength = req.headers.get("content-length"); if (contentLength) { if (req.headers.get("transfer-encoding")) throw new HTTPError({ status: 400 }); if (+contentLength > limit) return false; } const reader = req.clone().body.getReader(); let chunk = await reader.read(); let size = 0; while (!chunk.done) { size += chunk.value.byteLength; if (size > limit) { reader.cancel(); return false; } chunk = await reader.read(); } return true; } function onRequest(hook) { return async function _onRequestMiddleware(event) { await hook(event); }; } function onResponse(hook) { return async function _onResponseMiddleware(event, next) { const response = await toResponse(await next(), event); return await hook(response, event) || response; }; } function onError(hook) { return async (event, next) => { try { return await next(); } catch (rawError) { const isHTTPError = HTTPError.isError(rawError); const error = isHTTPError ? rawError : new HTTPError(rawError); if (!isHTTPError) { error.unhandled = true; if (rawError?.stack) error.stack = rawError.stack; } const hookResponse = await hook(error, event); if (hookResponse !== void 0) return hookResponse; throw error; } }; } function bodyLimit(limit) { return async (event, next) => { await assertBodySize(event, limit); return next(); }; } const ignoredHeaders = /* @__PURE__ */ new Set([ "transfer-encoding", "accept-encoding", "connection", "keep-alive", "upgrade", "expect", "te", "trailer", "host", "proxy-authorization", "proxy-connection" ]); function connectionTokens(connection) { return new Set((connection || "").toLowerCase().split(",").map((name) => name.trim()).filter(Boolean)); } const ignoredResponseHeaders = /* @__PURE__ */ new Set([ "content-encoding", "content-length", "transfer-encoding", "connection", "keep-alive", "proxy-authenticate", "proxy-connection", "upgrade", "trailer", "te" ]); function rewriteCookieProperty(header, map, property) { const _map = typeof map === "string" ? { "*": map } : map; return header.replace(new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"), (match, prefix, previousValue) => { let newValue; if (Object.hasOwn(_map, previousValue)) newValue = _map[previousValue]; else if (Object.hasOwn(_map, "*")) newValue = _map["*"]; else return match; return newValue ? prefix + newValue : ""; }); } function applyXForwardedHeaders(headers, event) { const merged = headers instanceof Headers ? headers : new Headers(headers); const ip = event.req.ip; if (ip && !merged.has("x-forwarded-for")) merged.set("x-forwarded-for", ip); const proto = event.url.protocol.slice(0, -1); if (proto && !merged.has("x-forwarded-proto")) merged.set("x-forwarded-proto", proto); if (!merged.has("x-forwarded-host")) merged.set("x-forwarded-host", event.url.host); if (!merged.has("x-forwarded-port")) merged.set("x-forwarded-port", event.url.port || (proto === "https" ? "443" : "80")); return merged; } function rewriteLocationHeaders(headers, rewrite, targetOrigin, requestOrigin) { const rewriteValue = (value) => rewrite === true ? rewriteOrigin(value, targetOrigin, requestOrigin) : rewritePrefix(value, rewrite); const location = headers.get("location"); if (location) { const rewritten = rewriteValue(location); if (rewritten) headers.set("location", rewritten); } const refresh = headers.get("refresh"); if (refresh) { const match = refresh.match(/^(\s*(?:[\d.]+\s*[;,]\s*)?url\s*=\s*)(['"]?)(.*?)\2(\s*)$/i); const rewritten = match && rewriteValue(match[3]); if (rewritten) headers.set("refresh", match[1] + match[2] + rewritten + match[2] + match[4]); } } function rewriteOrigin(value, targetOrigin, requestOrigin) { if (!targetOrigin || targetOrigin === requestOrigin) return; const url = value.startsWith("//") ? URL.canParse(value, targetOrigin) ? new URL(value, targetOrigin) : void 0 : URL.canParse(value) ? new URL(value) : void 0; if (!url || url.origin !== targetOrigin) return; return requestOrigin + url.pathname + url.search + url.hash; } function rewritePrefix(value, map) { for (const prefix of Object.keys(map)) if (value.startsWith(prefix)) return map[prefix] + value.slice(prefix.length); } function abortable(run, signal) { if (signal.aborted) return Promise.reject(signal.reason); return new Promise((resolve, reject) => { const onAbort = () => reject(signal.reason); signal.addEventListener("abort", onAbort, { once: true }); Promise.resolve(run()).then((value) => { signal.removeEventListener("abort", onAbort); resolve(value); }, (error) => { signal.removeEventListener("abort", onAbort); reject(error); }); }); } function mergeHeaders(defaults, ...inputs) { const _inputs = inputs.filter(Boolean); if (_inputs.length === 0) return defaults; const merged = new Headers(defaults); for (const input of _inputs) { const entries = Array.isArray(input) ? input : typeof input.entries === "function" ? input.entries() : Object.entries(input); for (const [key, value] of entries) if (value !== void 0) merged.set(key, value); } return merged; } async function proxyRequest(event, target, opts = {}) { const method = opts.fetchOptions?.method || event.req.method; const methodUpper = method.toUpperCase(); const incomingBody = event.req.body; const requestBody = incomingBody != null && methodUpper !== "GET" && methodUpper !== "HEAD" ? incomingBody : void 0; const fetchHeaders = mergeHeaders(getProxyRequestHeaders(event, { host: target.startsWith("/"), forwardHeaders: opts.forwardHeaders, filterHeaders: opts.filterHeaders }), opts.fetchOptions?.headers, opts.headers); if (opts.fetchOptions && "body" in opts.fetchOptions || incomingBody && !requestBody) { if (fetchHeaders instanceof Headers) fetchHeaders.delete("content-length"); else if (!Array.isArray(fetchHeaders)) delete fetchHeaders["content-length"]; } const fetchBody = opts.fetchOptions?.body ?? requestBody; return proxy(event, target, { ...opts, fetchOptions: { method, body: requestBody, ...opts.fetchOptions, duplex: opts.fetchOptions?.duplex ?? (fetchBody != null ? "half" : void 0), headers: opts.xfwd ? applyXForwardedHeaders(fetchHeaders, event) : fetchHeaders } }); } async function proxy(event, target, opts = {}) { const signals = [event.req.signal]; if (opts.fetchOptions?.signal) signals.push(opts.fetchOptions.signal); let timeoutId; if (opts.timeout > 0 && Number.isFinite(opts.timeout)) { const timeoutController = new AbortController(); timeoutId = setTimeout(() => timeoutController.abort(new DOMException("Proxy request timed out", "TimeoutError")), Math.min(Math.max(Math.trunc(opts.timeout), 1), 2147483647)); signals.push(timeoutController.signal); } const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0]; const fetchOptions = { headers: opts.headers, ...opts.fetchOptions, redirect: opts.fetchOptions?.redirect ?? "manual", signal }; let response; try { response = target[0] === "/" ? await abortable(() => event.app.fetch(createSubRequest(event, target, fetchOptions)), signal) : await fetch(target, fetchOptions); } catch (error) { if ((signal.aborted ? signal.reason : void 0)?.name === "TimeoutError" || error?.name === "TimeoutError") throw new HTTPError({ status: 504, statusText: "Gateway Timeout", cause: error }); if (signal.aborted || error?.name === "AbortError") { if (opts.propagateAbortError) throw error; if (event.req.signal.aborted) return new HTTPResponse(null, { status: 499, statusText: "Client Closed Request" }); } throw new HTTPError({ status: 502, cause: error }); } finally { if (timeoutId !== void 0) clearTimeout(timeoutId); } if (response.type === "opaqueredirect") throw new HTTPError({ status: 502, message: "Cannot relay an opaque redirect response on this runtime. Set `fetchOptions: { redirect: \"follow\" }` to follow upstream redirects instead." }); if (response.type === "opaque" || response.type === "error" || response.stat