UNPKG

h3

Version:

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

1,780 lines (1,751 loc) 73.9 kB
'use strict'; const ufo = require('ufo'); const cookieEs = require('cookie-es'); const radix3 = require('radix3'); const destr = require('destr'); const defu = require('defu'); const crypto = require('uncrypto'); const ironWebcrypto = require('iron-webcrypto'); const nodeMockHttp = require('node-mock-http'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const destr__default = /*#__PURE__*/_interopDefaultCompat(destr); const crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto); function useBase(base, handler) { base = ufo.withoutTrailingSlash(base); if (!base || base === "/") { return handler; } return eventHandler(async (event) => { event.node.req.originalUrl = event.node.req.originalUrl || event.node.req.url || "/"; const _path = event._path || event.node.req.url || "/"; event._path = ufo.withoutBase(event.path || "/", base); event.node.req.url = event._path; try { return await handler(event); } finally { event._path = event.node.req.url = _path; } }); } function hasProp(obj, prop) { try { return prop in obj; } catch { return false; } } class H3Error extends Error { static __h3_error__ = true; statusCode = 500; fatal = false; unhandled = false; statusMessage; data; cause; constructor(message, opts = {}) { super(message, opts); if (opts.cause && !this.cause) { this.cause = opts.cause; } } toJSON() { const obj = { message: this.message, statusCode: sanitizeStatusCode(this.statusCode, 500) }; if (this.statusMessage) { obj.statusMessage = sanitizeStatusMessage(this.statusMessage); } if (this.data !== void 0) { obj.data = this.data; } return obj; } } function createError(input) { if (typeof input === "string") { return new H3Error(input); } if (isError(input)) { return input; } const err = new H3Error(input.message ?? input.statusMessage ?? "", { cause: input.cause || input }); if (hasProp(input, "stack")) { try { Object.defineProperty(err, "stack", { get() { return input.stack; } }); } catch { try { err.stack = input.stack; } catch { } } } if (input.data) { err.data = input.data; } if (input.statusCode) { err.statusCode = sanitizeStatusCode(input.statusCode, err.statusCode); } else if (input.status) { err.statusCode = sanitizeStatusCode(input.status, err.statusCode); } if (input.statusMessage) { err.statusMessage = input.statusMessage; } else if (input.statusText) { err.statusMessage = input.statusText; } if (err.statusMessage) { const originalMessage = err.statusMessage; const sanitizedMessage = sanitizeStatusMessage(err.statusMessage); if (sanitizedMessage !== originalMessage) { console.warn( "[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future, `statusMessage` will be sanitized by default." ); } } if (input.fatal !== void 0) { err.fatal = input.fatal; } if (input.unhandled !== void 0) { err.unhandled = input.unhandled; } return err; } function sendError(event, error, debug) { if (event.handled) { return; } const h3Error = isError(error) ? error : createError(error); const responseBody = { statusCode: h3Error.statusCode, statusMessage: h3Error.statusMessage, stack: [], data: h3Error.data }; if (debug) { responseBody.stack = (h3Error.stack || "").split("\n").map((l) => l.trim()); } if (event.handled) { return; } const _code = Number.parseInt(h3Error.statusCode); setResponseStatus(event, _code, h3Error.statusMessage); event.node.res.setHeader("content-type", MIMES.json); event.node.res.end(JSON.stringify(responseBody, void 0, 2)); } function isError(input) { return input?.constructor?.__h3_error__ === true; } function parse(multipartBodyBuffer, boundary) { let lastline = ""; let state = 0 /* INIT */; let buffer = []; const allParts = []; let currentPartHeaders = []; for (let i = 0; i < multipartBodyBuffer.length; i++) { const prevByte = i > 0 ? multipartBodyBuffer[i - 1] : null; const currByte = multipartBodyBuffer[i]; const newLineChar = currByte === 10 || currByte === 13; if (!newLineChar) { lastline += String.fromCodePoint(currByte); } const newLineDetected = currByte === 10 && prevByte === 13; if (0 /* INIT */ === state && newLineDetected) { if ("--" + boundary === lastline) { state = 1 /* READING_HEADERS */; } lastline = ""; } else if (1 /* READING_HEADERS */ === state && newLineDetected) { if (lastline.length > 0) { const i2 = lastline.indexOf(":"); if (i2 > 0) { const name = lastline.slice(0, i2).toLowerCase(); const value = lastline.slice(i2 + 1).trim(); currentPartHeaders.push([name, value]); } } else { state = 2 /* READING_DATA */; buffer = []; } lastline = ""; } else if (2 /* READING_DATA */ === state) { if (lastline.length > boundary.length + 4) { lastline = ""; } if ("--" + boundary === lastline) { const j = buffer.length - lastline.length; const part = buffer.slice(0, j - 1); allParts.push(process(part, currentPartHeaders)); buffer = []; currentPartHeaders = []; lastline = ""; state = 3 /* READING_PART_SEPARATOR */; } else { buffer.push(currByte); } if (newLineDetected) { lastline = ""; } } else if (3 /* READING_PART_SEPARATOR */ === state && newLineDetected) { state = 1 /* READING_HEADERS */; } } return allParts; } function process(data, headers) { const dataObj = {}; const contentDispositionHeader = headers.find((h) => h[0] === "content-disposition")?.[1] || ""; for (const i of contentDispositionHeader.split(";")) { const s = i.split("="); if (s.length !== 2) { continue; } const key = (s[0] || "").trim(); if (key === "name" || key === "filename") { const _value = (s[1] || "").trim().replace(/"/g, ""); dataObj[key] = Buffer.from(_value, "latin1").toString("utf8"); } } const contentType = headers.find((h) => h[0] === "content-type")?.[1] || ""; if (contentType) { dataObj.type = contentType; } dataObj.data = Buffer.from(data); return dataObj; } async function validateData(data, fn) { try { const res = await fn(data); if (res === false) { throw createValidationError(); } if (res === true) { return data; } return res ?? data; } catch (error) { throw createValidationError(error); } } function createValidationError(validateError) { throw createError({ status: 400, statusMessage: "Validation Error", message: validateError?.message || "Validation Error", data: validateError }); } function getQuery(event) { return ufo.getQuery(event.path || ""); } function getValidatedQuery(event, validate) { const query = getQuery(event); return validateData(query, validate); } function getRouterParams(event, opts = {}) { let params = event.context.params || {}; if (opts.decode) { params = { ...params }; for (const key in params) { params[key] = ufo.decode(params[key]); } } return params; } function getValidatedRouterParams(event, validate, opts = {}) { const routerParams = getRouterParams(event, opts); return validateData(routerParams, validate); } function getRouterParam(event, name, opts = {}) { const params = getRouterParams(event, opts); return params[name]; } function getMethod(event, defaultMethod = "GET") { return (event.node.req.method || defaultMethod).toUpperCase(); } function isMethod(event, expected, allowHead) { if (allowHead && event.method === "HEAD") { return true; } if (typeof expected === "string") { if (event.method === expected) { return true; } } else if (expected.includes(event.method)) { return true; } return false; } function assertMethod(event, expected, allowHead) { if (!isMethod(event, expected, allowHead)) { throw createError({ statusCode: 405, statusMessage: "HTTP method is not allowed." }); } } function getRequestHeaders(event) { const _headers = {}; for (const key in event.node.req.headers) { const val = event.node.req.headers[key]; _headers[key] = Array.isArray(val) ? val.filter(Boolean).join(", ") : val; } return _headers; } const getHeaders = getRequestHeaders; function getRequestHeader(event, name) { const headers = getRequestHeaders(event); const value = headers[name.toLowerCase()]; return value; } const getHeader = getRequestHeader; function getRequestHost(event, opts = {}) { if (opts.xForwardedHost) { const _header = event.node.req.headers["x-forwarded-host"]; const xForwardedHost = (_header || "").split(",").shift()?.trim(); if (xForwardedHost) { return xForwardedHost; } } return event.node.req.headers.host || "localhost"; } function getRequestProtocol(event, opts = {}) { if (opts.xForwardedProto !== false && event.node.req.headers["x-forwarded-proto"] === "https") { return "https"; } return event.node.req.connection?.encrypted ? "https" : "http"; } const DOUBLE_SLASH_RE = /[/\\]{2,}/g; function getRequestPath(event) { const path = (event.node.req.url || "/").replace(DOUBLE_SLASH_RE, "/"); return path; } function getRequestURL(event, opts = {}) { const host = getRequestHost(event, opts); const protocol = getRequestProtocol(event, opts); const path = (event.node.req.originalUrl || event.path).replace( /^[/\\]+/g, "/" ); return new URL(path, `${protocol}://${host}`); } function toWebRequest(event) { return event.web?.request || new Request(getRequestURL(event), { // @ts-ignore Undici option duplex: "half", method: event.method, headers: event.headers, body: getRequestWebStream(event) }); } function getRequestIP(event, opts = {}) { if (event.context.clientAddress) { return event.context.clientAddress; } if (opts.xForwardedFor) { const xForwardedFor = getRequestHeader(event, "x-forwarded-for")?.split(",").shift()?.trim(); if (xForwardedFor) { return xForwardedFor; } } if (event.node.req.socket.remoteAddress) { return event.node.req.socket.remoteAddress; } } const RawBodySymbol = Symbol.for("h3RawBody"); const ParsedBodySymbol = Symbol.for("h3ParsedBody"); const PayloadMethods$1 = ["PATCH", "POST", "PUT", "DELETE"]; function readRawBody(event, encoding = "utf8") { assertMethod(event, PayloadMethods$1); const _rawBody = event._requestBody || event.web?.request?.body || event.node.req[RawBodySymbol] || event.node.req.rawBody || event.node.req.body; if (_rawBody) { const promise2 = Promise.resolve(_rawBody).then((_resolved) => { if (Buffer.isBuffer(_resolved)) { return _resolved; } if (typeof _resolved.pipeTo === "function") { return new Promise((resolve, reject) => { const chunks = []; _resolved.pipeTo( new WritableStream({ write(chunk) { chunks.push(chunk); }, close() { resolve(Buffer.concat(chunks)); }, abort(reason) { reject(reason); } }) ).catch(reject); }); } else if (typeof _resolved.pipe === "function") { return new Promise((resolve, reject) => { const chunks = []; _resolved.on("data", (chunk) => { chunks.push(chunk); }).on("end", () => { resolve(Buffer.concat(chunks)); }).on("error", reject); }); } if (_resolved.constructor === Object) { return Buffer.from(JSON.stringify(_resolved)); } if (_resolved instanceof URLSearchParams) { return Buffer.from(_resolved.toString()); } if (_resolved instanceof FormData) { return new Response(_resolved).bytes().then((uint8arr) => Buffer.from(uint8arr)); } return Buffer.from(_resolved); }); return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2; } if (!Number.parseInt(event.node.req.headers["content-length"] || "") && !String(event.node.req.headers["transfer-encoding"] ?? "").split(",").map((e) => e.trim()).filter(Boolean).includes("chunked")) { return Promise.resolve(void 0); } const promise = event.node.req[RawBodySymbol] = new Promise( (resolve, reject) => { const bodyData = []; event.node.req.on("error", (err) => { reject(err); }).on("data", (chunk) => { bodyData.push(chunk); }).on("end", () => { resolve(Buffer.concat(bodyData)); }); } ); const result = encoding ? promise.then((buff) => buff.toString(encoding)) : promise; return result; } async function readBody(event, options = {}) { const request = event.node.req; if (hasProp(request, ParsedBodySymbol)) { return request[ParsedBodySymbol]; } const contentType = request.headers["content-type"] || ""; const body = await readRawBody(event); let parsed; if (contentType === "application/json") { parsed = _parseJSON(body, options.strict ?? true); } else if (contentType.startsWith("application/x-www-form-urlencoded")) { parsed = _parseURLEncodedBody(body); } else if (contentType.startsWith("text/")) { parsed = body; } else { parsed = _parseJSON(body, options.strict ?? false); } request[ParsedBodySymbol] = parsed; return parsed; } async function readValidatedBody(event, validate) { const _body = await readBody(event, { strict: true }); return validateData(_body, validate); } async function readMultipartFormData(event) { const contentType = getRequestHeader(event, "content-type"); if (!contentType || !contentType.startsWith("multipart/form-data")) { return; } const boundary = contentType.match(/boundary=([^;]*)(;|$)/i)?.[1]; if (!boundary) { return; } const body = await readRawBody(event, false); if (!body) { return; } return parse(body, boundary); } async function readFormData(event) { return await toWebRequest(event).formData(); } function getRequestWebStream(event) { if (!PayloadMethods$1.includes(event.method)) { return; } const bodyStream = event.web?.request?.body || event._requestBody; if (bodyStream) { return bodyStream; } const _hasRawBody = RawBodySymbol in event.node.req || "rawBody" in event.node.req || "body" in event.node.req || "__unenv__" in event.node.req; if (_hasRawBody) { return new ReadableStream({ async start(controller) { const _rawBody = await readRawBody(event, false); if (_rawBody) { controller.enqueue(_rawBody); } controller.close(); } }); } return new ReadableStream({ start: (controller) => { event.node.req.on("data", (chunk) => { controller.enqueue(chunk); }); event.node.req.on("end", () => { controller.close(); }); event.node.req.on("error", (err) => { controller.error(err); }); } }); } function _parseJSON(body = "", strict) { if (!body) { return void 0; } try { return destr__default(body, { strict }); } catch { throw createError({ statusCode: 400, statusMessage: "Bad Request", message: "Invalid JSON body" }); } } function _parseURLEncodedBody(body) { const form = new URLSearchParams(body); const parsedForm = /* @__PURE__ */ Object.create(null); for (const [key, value] of form.entries()) { if (hasProp(parsedForm, key)) { if (!Array.isArray(parsedForm[key])) { parsedForm[key] = [parsedForm[key]]; } parsedForm[key].push(value); } else { parsedForm[key] = value; } } return parsedForm; } function handleCacheHeaders(event, opts) { const cacheControls = ["public", ...opts.cacheControls || []]; let cacheMatched = false; if (opts.maxAge !== void 0) { cacheControls.push(`max-age=${+opts.maxAge}`, `s-maxage=${+opts.maxAge}`); } if (opts.modifiedTime) { const modifiedTime = new Date(opts.modifiedTime); const ifModifiedSince = event.node.req.headers["if-modified-since"]; event.node.res.setHeader("last-modified", modifiedTime.toUTCString()); if (ifModifiedSince && new Date(ifModifiedSince) >= modifiedTime) { cacheMatched = true; } } if (opts.etag) { event.node.res.setHeader("etag", opts.etag); const ifNonMatch = event.node.req.headers["if-none-match"]; if (ifNonMatch === opts.etag) { cacheMatched = true; } } event.node.res.setHeader("cache-control", cacheControls.join(", ")); if (cacheMatched) { event.node.res.statusCode = 304; if (!event.handled) { event.node.res.end(); } return true; } return false; } const MIMES = { html: "text/html", json: "application/json" }; 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 = Number.parseInt(statusCode, 10); } if (statusCode < 100 || statusCode > 999) { return defaultStatusCode; } return statusCode; } function getDistinctCookieKey(name, opts) { return [name, opts.domain || "", opts.path || "/"].join(";"); } function parseCookies(event) { return cookieEs.parse(event.node.req.headers.cookie || ""); } function getCookie(event, name) { return parseCookies(event)[name]; } function setCookie(event, name, value, serializeOptions = {}) { if (!serializeOptions.path) { serializeOptions = { path: "/", ...serializeOptions }; } const newCookie = cookieEs.serialize(name, value, serializeOptions); const currentCookies = splitCookiesString( event.node.res.getHeader("set-cookie") ); if (currentCookies.length === 0) { event.node.res.setHeader("set-cookie", newCookie); return; } const newCookieKey = getDistinctCookieKey(name, serializeOptions); event.node.res.removeHeader("set-cookie"); for (const cookie of currentCookies) { const parsed = cookieEs.parseSetCookie(cookie); const key = getDistinctCookieKey(parsed.name, parsed); if (key === newCookieKey) { continue; } event.node.res.appendHeader("set-cookie", cookie); } event.node.res.appendHeader("set-cookie", newCookie); } function deleteCookie(event, name, serializeOptions) { setCookie(event, name, "", { ...serializeOptions, maxAge: 0 }); } function splitCookiesString(cookiesString) { if (Array.isArray(cookiesString)) { return cookiesString.flatMap((c) => splitCookiesString(c)); } if (typeof cookiesString !== "string") { return []; } const cookiesStrings = []; let pos = 0; let start; let ch; let lastComma; let nextStart; let cookiesSeparatorFound; const skipWhitespace = () => { while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { pos += 1; } return pos < cookiesString.length; }; const notSpecialChar = () => { ch = cookiesString.charAt(pos); return ch !== "=" && ch !== ";" && ch !== ","; }; while (pos < cookiesString.length) { start = pos; cookiesSeparatorFound = false; while (skipWhitespace()) { ch = cookiesString.charAt(pos); if (ch === ",") { lastComma = pos; pos += 1; skipWhitespace(); nextStart = pos; while (pos < cookiesString.length && notSpecialChar()) { pos += 1; } if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { cookiesSeparatorFound = true; pos = nextStart; cookiesStrings.push(cookiesString.slice(start, lastComma)); start = pos; } else { pos = lastComma + 1; } } else { pos += 1; } } if (!cookiesSeparatorFound || pos >= cookiesString.length) { cookiesStrings.push(cookiesString.slice(start)); } } return cookiesStrings; } function serializeIterableValue(value) { switch (typeof value) { case "string": { return value; } case "boolean": case "number": case "bigint": case "symbol": { return value.toString(); } case "function": case "undefined": { return void 0; } case "object": { if (value instanceof Uint8Array) { return value; } return JSON.stringify(value); } } } 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; } const defer = typeof setImmediate === "undefined" ? (fn) => fn() : setImmediate; function send(event, data, type) { if (type) { defaultContentType(event, type); } return new Promise((resolve) => { defer(() => { if (!event.handled) { event.node.res.end(data); } resolve(); }); }); } function sendNoContent(event, code) { if (event.handled) { return; } if (!code && event.node.res.statusCode !== 200) { code = event.node.res.statusCode; } const _code = sanitizeStatusCode(code, 204); if (_code === 204) { event.node.res.removeHeader("content-length"); } event.node.res.writeHead(_code); event.node.res.end(); } function setResponseStatus(event, code, text) { if (code) { event.node.res.statusCode = sanitizeStatusCode( code, event.node.res.statusCode ); } if (text) { event.node.res.statusMessage = sanitizeStatusMessage(text); } } function getResponseStatus(event) { return event.node.res.statusCode; } function getResponseStatusText(event) { return event.node.res.statusMessage; } function defaultContentType(event, type) { if (type && event.node.res.statusCode !== 304 && !event.node.res.getHeader("content-type")) { event.node.res.setHeader("content-type", type); } } function sendRedirect(event, location, code = 302) { event.node.res.statusCode = sanitizeStatusCode( code, event.node.res.statusCode ); event.node.res.setHeader("location", location); const encodedLoc = location.replace(/"/g, "%22"); const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`; return send(event, html, MIMES.html); } function getResponseHeaders(event) { return event.node.res.getHeaders(); } function getResponseHeader(event, name) { return event.node.res.getHeader(name); } function setResponseHeaders(event, headers) { for (const [name, value] of Object.entries(headers)) { event.node.res.setHeader( name, value ); } } const setHeaders = setResponseHeaders; function setResponseHeader(event, name, value) { event.node.res.setHeader(name, value); } const setHeader = setResponseHeader; function appendResponseHeaders(event, headers) { for (const [name, value] of Object.entries(headers)) { appendResponseHeader(event, name, value); } } const appendHeaders = appendResponseHeaders; function appendResponseHeader(event, name, value) { let current = event.node.res.getHeader(name); if (!current) { event.node.res.setHeader(name, value); return; } if (!Array.isArray(current)) { current = [current.toString()]; } event.node.res.setHeader(name, [...current, value]); } const appendHeader = appendResponseHeader; function clearResponseHeaders(event, headerNames) { if (headerNames && headerNames.length > 0) { for (const name of headerNames) { removeResponseHeader(event, name); } } else { for (const [name] of Object.entries(getResponseHeaders(event))) { removeResponseHeader(event, name); } } } function removeResponseHeader(event, name) { return event.node.res.removeHeader(name); } function isStream(data) { if (!data || typeof data !== "object") { return false; } if (typeof data.pipe === "function") { if (typeof data._read === "function") { return true; } if (typeof data.abort === "function") { return true; } } if (typeof data.pipeTo === "function") { return true; } return false; } function isWebResponse(data) { return typeof Response !== "undefined" && data instanceof Response; } function sendStream(event, stream) { if (!stream || typeof stream !== "object") { throw new Error("[h3] Invalid stream provided."); } event.node.res._data = stream; if (!event.node.res.socket) { event._handled = true; return Promise.resolve(); } if (hasProp(stream, "pipeTo") && typeof stream.pipeTo === "function") { return stream.pipeTo( new WritableStream({ write(chunk) { event.node.res.write(chunk); } }) ).then(() => { event.node.res.end(); }); } if (hasProp(stream, "pipe") && typeof stream.pipe === "function") { return new Promise((resolve, reject) => { stream.pipe(event.node.res); if (stream.on) { stream.on("end", () => { event.node.res.end(); resolve(); }); stream.on("error", (error) => { reject(error); }); } event.node.res.on("close", () => { if (stream.abort) { stream.abort(); } }); }); } throw new Error("[h3] Invalid or incompatible stream provided."); } const noop = () => { }; function writeEarlyHints(event, hints, cb = noop) { if (!event.node.res.socket) { cb(); return; } if (typeof hints === "string" || Array.isArray(hints)) { hints = { link: hints }; } if (hints.link) { hints.link = Array.isArray(hints.link) ? hints.link : hints.link.split(","); } const headers = Object.entries(hints).map( (e) => [e[0].toLowerCase(), e[1]] ); if (headers.length === 0) { cb(); return; } let hint = "HTTP/1.1 103 Early Hints"; if (hints.link) { hint += `\r Link: ${hints.link.join(", ")}`; } for (const [header, value] of headers) { if (header === "link") { continue; } hint += `\r ${header}: ${value}`; } if (event.node.res.socket) { event.node.res.socket.write( `${hint}\r \r `, "utf8", cb ); } else { cb(); } } function sendWebResponse(event, response) { for (const [key, value] of response.headers) { if (key === "set-cookie") { event.node.res.appendHeader(key, splitCookiesString(value)); } else { event.node.res.setHeader(key, value); } } if (response.status) { event.node.res.statusCode = sanitizeStatusCode( response.status, event.node.res.statusCode ); } if (response.statusText) { event.node.res.statusMessage = sanitizeStatusMessage(response.statusText); } if (response.redirected) { event.node.res.setHeader("location", response.url); } if (!response.body) { event.node.res.end(); return; } return sendStream(event, response.body); } function sendIterable(event, iterable, options) { const serializer = options?.serializer ?? serializeIterableValue; const iterator = coerceIterable(iterable); return sendStream( event, 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 resolveCorsOptions(options = {}) { const defaultOptions = { origin: "*", methods: "*", allowHeaders: "*", exposeHeaders: "*", credentials: false, maxAge: false, preflight: { statusCode: 204 } }; return defu.defu(options, defaultOptions); } function isPreflightRequest(event) { const origin = getRequestHeader(event, "origin"); const accessControlRequestMethod = getRequestHeader( event, "access-control-request-method" ); return event.method === "OPTIONS" && !!origin && !!accessControlRequestMethod; } function isCorsOriginAllowed(origin, options) { const { origin: originOption } = options; if (!origin || !originOption || originOption === "*" || originOption === "null") { return true; } if (Array.isArray(originOption)) { return originOption.some((_origin) => { if (_origin instanceof RegExp) { return _origin.test(origin); } return origin === _origin; }); } return originOption(origin); } function createOriginHeaders(event, options) { const { origin: originOption } = options; const origin = getRequestHeader(event, "origin"); if (!origin || !originOption || originOption === "*") { return { "access-control-allow-origin": "*" }; } if (typeof originOption === "string") { return { "access-control-allow-origin": originOption, vary: "origin" }; } return isCorsOriginAllowed(origin, options) ? { "access-control-allow-origin": origin, vary: "origin" } : {}; } function createMethodsHeaders(options) { const { methods } = options; if (!methods) { return {}; } if (methods === "*") { return { "access-control-allow-methods": "*" }; } return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {}; } function createCredentialsHeaders(options) { const { credentials } = options; if (credentials) { return { "access-control-allow-credentials": "true" }; } return {}; } function createAllowHeaderHeaders(event, options) { const { allowHeaders } = options; if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) { const header = getRequestHeader(event, "access-control-request-headers"); return header ? { "access-control-allow-headers": header, vary: "access-control-request-headers" } : {}; } return { "access-control-allow-headers": allowHeaders.join(","), vary: "access-control-request-headers" }; } function createExposeHeaders(options) { const { exposeHeaders } = options; if (!exposeHeaders) { return {}; } if (exposeHeaders === "*") { return { "access-control-expose-headers": exposeHeaders }; } return { "access-control-expose-headers": exposeHeaders.join(",") }; } function appendCorsPreflightHeaders(event, options) { appendHeaders(event, createOriginHeaders(event, options)); appendHeaders(event, createCredentialsHeaders(options)); appendHeaders(event, createExposeHeaders(options)); appendHeaders(event, createMethodsHeaders(options)); appendHeaders(event, createAllowHeaderHeaders(event, options)); } function appendCorsHeaders(event, options) { appendHeaders(event, createOriginHeaders(event, options)); appendHeaders(event, createCredentialsHeaders(options)); appendHeaders(event, createExposeHeaders(options)); } function handleCors(event, options) { const _options = resolveCorsOptions(options); if (isPreflightRequest(event)) { appendCorsPreflightHeaders(event, options); sendNoContent(event, _options.preflight.statusCode); return true; } appendCorsHeaders(event, options); return false; } async function getRequestFingerprint(event, opts = {}) { const fingerprint = []; if (opts.ip !== false) { fingerprint.push( getRequestIP(event, { xForwardedFor: opts.xForwardedFor }) ); } if (opts.method === true) { fingerprint.push(event.method); } if (opts.path === true) { fingerprint.push(event.path); } if (opts.userAgent === true) { fingerprint.push(getRequestHeader(event, "user-agent")); } const fingerprintString = fingerprint.filter(Boolean).join("|"); if (!fingerprintString) { return null; } if (opts.hash === false) { return fingerprintString; } const buffer = await crypto__default.subtle.digest( opts.hash || "SHA-1", new TextEncoder().encode(fingerprintString) ); const hash = [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, "0")).join(""); return hash; } const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]); const ignoredHeaders = /* @__PURE__ */ new Set([ "transfer-encoding", "accept-encoding", "connection", "keep-alive", "upgrade", "expect", "host", "accept" ]); async function proxyRequest(event, target, opts = {}) { let body; let duplex; if (PayloadMethods.has(event.method)) { if (opts.streamRequest) { body = getRequestWebStream(event); duplex = "half"; } else { body = await readRawBody(event, false).catch(() => void 0); } } const method = opts.fetchOptions?.method || event.method; const fetchHeaders = mergeHeaders( getProxyRequestHeaders(event, { host: target.startsWith("/") }), opts.fetchOptions?.headers, opts.headers ); return sendProxy(event, target, { ...opts, fetchOptions: { method, body, duplex, ...opts.fetchOptions, headers: fetchHeaders } }); } async function sendProxy(event, target, opts = {}) { let response; try { response = await _getFetch(opts.fetch)(target, { headers: opts.headers, ignoreResponseError: true, // make $ofetch.raw transparent ...opts.fetchOptions }); } catch (error) { throw createError({ status: 502, statusMessage: "Bad Gateway", cause: error }); } event.node.res.statusCode = sanitizeStatusCode( response.status, event.node.res.statusCode ); event.node.res.statusMessage = sanitizeStatusMessage(response.statusText); const cookies = []; for (const [key, value] of response.headers.entries()) { if (key === "content-encoding") { continue; } if (key === "content-length") { continue; } if (key === "set-cookie") { cookies.push(...splitCookiesString(value)); continue; } event.node.res.setHeader(key, value); } if (cookies.length > 0) { event.node.res.setHeader( "set-cookie", cookies.map((cookie) => { if (opts.cookieDomainRewrite) { cookie = rewriteCookieProperty( cookie, opts.cookieDomainRewrite, "domain" ); } if (opts.cookiePathRewrite) { cookie = rewriteCookieProperty( cookie, opts.cookiePathRewrite, "path" ); } return cookie; }) ); } if (opts.onResponse) { await opts.onResponse(event, response); } if (response._data !== void 0) { return response._data; } if (event.handled) { return; } if (opts.sendStream === false) { const data = new Uint8Array(await response.arrayBuffer()); return event.node.res.end(data); } if (response.body) { for await (const chunk of response.body) { event.node.res.write(chunk); } } return event.node.res.end(); } function getProxyRequestHeaders(event, opts) { const headers = /* @__PURE__ */ Object.create(null); const reqHeaders = getRequestHeaders(event); for (const name in reqHeaders) { if (!ignoredHeaders.has(name) || name === "host" && opts?.host) { headers[name] = reqHeaders[name]; } } return headers; } function fetchWithEvent(event, req, init, options) { return _getFetch(options?.fetch)(req, { ...init, context: init?.context || event.context, headers: { ...getProxyRequestHeaders(event, { host: typeof req === "string" && req.startsWith("/") }), ...init?.headers } }); } function _getFetch(_fetch) { if (_fetch) { return _fetch; } if (globalThis.fetch) { return globalThis.fetch; } throw new Error( "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js." ); } 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 (previousValue in _map) { newValue = _map[previousValue]; } else if ("*" in _map) { newValue = _map["*"]; } else { return match; } return newValue ? prefix + newValue : ""; } ); } 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; } const getSessionPromise = Symbol("getSession"); const DEFAULT_NAME = "h3"; const DEFAULT_COOKIE = { path: "/", secure: true, httpOnly: true }; async function useSession(event, config) { const sessionName = config.name || DEFAULT_NAME; await getSession(event, config); const sessionManager = { get id() { return event.context.sessions?.[sessionName]?.id; }, get data() { return event.context.sessions?.[sessionName]?.data || {}; }, update: async (update) => { if (!isEvent(event)) { throw new Error("[h3] Cannot update read-only session."); } await updateSession(event, config, update); return sessionManager; }, clear: () => { if (!isEvent(event)) { throw new Error("[h3] Cannot clear read-only session."); } clearSession(event, config); return Promise.resolve(sessionManager); } }; return sessionManager; } async function getSession(event, config) { const sessionName = config.name || DEFAULT_NAME; if (!event.context.sessions) { event.context.sessions = /* @__PURE__ */ Object.create(null); } const existingSession = event.context.sessions[sessionName]; if (existingSession) { return existingSession[getSessionPromise] || existingSession; } const session = { id: "", createdAt: 0, data: /* @__PURE__ */ Object.create(null) }; event.context.sessions[sessionName] = session; let sealedSession; if (config.sessionHeader !== false) { const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`; const headerValue = _getReqHeader(event, headerName); if (typeof headerValue === "string") { sealedSession = headerValue; } } if (!sealedSession) { const cookieHeader = _getReqHeader(event, "cookie"); if (cookieHeader) { sealedSession = cookieEs.parse(cookieHeader + "")[sessionName]; } } if (sealedSession) { const promise = unsealSession(event, config, sealedSession).catch(() => { }).then((unsealed) => { Object.assign(session, unsealed); delete event.context.sessions[sessionName][getSessionPromise]; return session; }); event.context.sessions[sessionName][getSessionPromise] = promise; await promise; } if (!session.id) { if (!isEvent(event)) { throw new Error( "Cannot initialize a new session. Make sure using `useSession(event)` in main handler." ); } session.id = config.generateId?.() ?? (config.crypto || crypto__default).randomUUID(); session.createdAt = Date.now(); await updateSession(event, config); } return session; } function _getReqHeader(event, name) { if (event.node) { return event.node?.req.headers[name]; } if (event.request) { return event.request.headers?.get(name); } if (event.headers) { return event.headers.get(name); } } async function updateSession(event, config, update) { const sessionName = config.name || DEFAULT_NAME; const session = event.context.sessions?.[sessionName] || await getSession(event, config); if (typeof update === "function") { update = update(session.data); } if (update) { Object.assign(session.data, update); } if (config.cookie !== false) { const sealed = await sealSession(event, config); setCookie(event, sessionName, sealed, { ...DEFAULT_COOKIE, expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0, ...config.cookie }); } return session; } async function sealSession(event, config) { const sessionName = config.name || DEFAULT_NAME; const session = event.context.sessions?.[sessionName] || await getSession(event, config); const sealed = await ironWebcrypto.seal(config.crypto || crypto__default, session, config.password, { ...ironWebcrypto.defaults, ttl: config.maxAge ? config.maxAge * 1e3 : 0, ...config.seal }); return sealed; } async function unsealSession(_event, config, sealed) { const unsealed = await ironWebcrypto.unseal( config.crypto || crypto__default, sealed, config.password, { ...ironWebcrypto.defaults, ttl: config.maxAge ? config.maxAge * 1e3 : 0, ...config.seal } ); if (config.maxAge) { const age = Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY); if (age > config.maxAge * 1e3) { throw new Error("Session expired!"); } } return unsealed; } function clearSession(event, config) { const sessionName = config.name || DEFAULT_NAME; if (event.context.sessions?.[sessionName]) { delete event.context.sessions[sessionName]; } setCookie(event, sessionName, "", { ...DEFAULT_COOKIE, ...config.cookie }); return Promise.resolve(); } function formatEventStreamMessage(message) { let result = ""; if (message.id) { result += `id: ${message.id} `; } if (message.event) { result += `event: ${message.event} `; } if (typeof message.retry === "number" && Number.isInteger(message.retry)) { result += `retry: ${message.retry} `; } result += `data: ${message.data} `; return result; } function formatEventStreamMessages(messages) { let result = ""; for (const msg of messages) { result += formatEventStreamMessage(msg); } return result; } function setEventStreamHeaders(event) { const headers = { "Content-Type": "text/event-stream", "Cache-Control": "private, no-cache, no-store, no-transform, must-revalidate, max-age=0", "X-Accel-Buffering": "no" // prevent nginx from buffering the response }; if (!isHttp2Request(event)) { headers.Connection = "keep-alive"; } setResponseHeaders(event, headers); } function isHttp2Request(event) { return getHeader(event, ":path") !== void 0 && getHeader(event, ":method") !== void 0; } class EventStream { _h3Event; _transformStream = new TransformStream(); _writer; _encoder = new TextEncoder(); _writerIsClosed = false; _paused = false; _unsentData; _disposed = false; _handled = false; constructor(event, opts = {}) { this._h3Event = event; this._writer = this._transformStream.writable.getWriter(); this._writer.closed.then(() => { this._writerIsClosed = true; }); if (opts.autoclose !== false) { this._h3Event.node.req.on("close", () => this.close()); } } async push(message) { if (typeof message === "string") { await this._sendEvent({ data: message }); return; } if (Array.isArray(message)) { if (message.length === 0) { return; } if (typeof message[0] === "string") { const msgs = []; for (const item of message) { msgs.push({ data: item }); } await this._sendEvents(msgs); return; } await this._sendEvents(message); return; } await this._sendEvent(message); } async _sendEvent(message) { if (this._writerIsClosed) { return; } if (this._paused && !this._unsentData) { this._unsentData = formatEventStreamMessage(message); return; } if (this._paused) { this._unsentData += formatEventStreamMessage(message); return; } await this._writer.write(this._encoder.encode(formatEventStreamMessage(message))).catch(); } async _sendEvents(messages) { if (this._writerIsClosed) { return; } const payload = formatEventStreamMessages(messages); if (this._paused && !this._unsentData) { this._unsentData = payload; return; } if (this._paused) { this._unsentData += payload; return; } await this._writer.write(this._encoder.encode(payload)).catch(); } pause() { this._paused = true; } get isPaused() { return this._paused; } async resume() { this._paused = false; await this.flush(); } async flush() { if (this._writerIsClosed) { return; } if (this._unsentData?.length) { await this._writer.write(this._encoder.encode(this._unsentData)); this._unsentData = void 0; } } /** * Close the stream and the connection if the stream is being sent to the client */ async close() { if (this._disposed) { return; } if (!this._writerIsClosed) { try { await this._writer.close(); } catch { } } if (this._h3Event._handled && this._handled && !this._h3Event.node.res.closed) { this._h3Event.node.res.end(); } this._disposed = true; } /** * Triggers callback when the writable stream is closed. * It is also triggered after calling the `close()` method. */ onClosed(cb) { this._writer.closed.then(cb); } async send() { setEventStreamHeaders(this._h3Event); setResponseStatus(this._h3Event, 200); this._h3Event._handled = true; this._handled = true; await sendStream(this._h3Event, this._transformStream.readable); } } function createEventStream(event, opts) { return new EventStream(event, opts); } async function serveStatic(event, options) { if (event.method !== "GET" && event.method !== "HEAD") { if (!options.fallthrough) { throw createError({ statusMessage: "Method Not Allowed", statusCode: 405 }); } return false; } const originalId = ufo.decodePath( ufo.withLeadingSlash(ufo.withoutTrailingSlash(ufo.parseURL(event.path).pathname)) ); const acceptEncodings = parseAcceptEncoding( getRequestHeader(event, "accept-encoding"), options.encodings ); if (acceptEncodings.length > 1) { setResponseHeader(event, "vary", "accept-encoding"); } let id = originalId; let meta; const _ids = idSearchPaths( originalId, acceptEncodings, options.indexNames || ["/index.html"] ); for (const _id of _ids) { const _meta = await options.getMeta(_id); if (_meta) { meta = _meta; id = _id; break; } } if (!meta) { if (!options.fallthrough) { throw createError({ statusCode: 404 }); } return false; } if (meta.etag && !getResponseHeader(event, "etag")) { setResponseHeader(event, "etag", meta.etag); } const ifNotMatch = meta.etag && getRequestHeader(event, "if-none-match") === meta.etag; if (ifNotMatch) { setResponseStatus(event, 304, "Not Modified"); return send(event, ""); } if (meta.mtime) { const mtimeDate = new Date(meta.mtime); const ifModifiedSinceH = getRequestHeader(event, "if-modified-since"); if (ifModifiedSinceH && new Date(ifModifiedSinceH) >= mtimeDate) { setResponseStatus(event, 304, "Not Modified"); return send(event, null); } if (!getResponseHeader(event, "last-modified")) { setResponseHeader(event, "last-modified", mtimeDate.toUTCString()); } } if (meta.type && !getResponseHeader(event, "content-type")) { setResponseHeader(event, "content-type", meta.type); } if (meta.encoding && !getResponseHeader(event, "content-encoding")) { setResponseHeader(event, "content-encoding", meta.encoding); } if (meta.size !== void 0 && meta.size > 0 && !getResponseHeader(event, "content-length")) { setResponseHeader(event, "content-length", meta.size); } if (event.method === "HEAD") { return send(event, null); } const contents = await options.getContents(id); return isStream(contents) ? sendStream(event, contents) : send(event, contents); } function parseAcceptEncoding(header, encodingMap) { if (!encodingMap || !header) { return []; } return String(header || "").split(",").map((e) => encodingMap[e.trim()]).filter(Boolean); } function idSearchPaths(id, encodings, indexNames) { const ids = []; for (const suffix of ["", ...indexNames]) { for (const encoding of [...encodings, ""]) { ids.push(`${id}${suffix}${encoding}`); } } return ids; } function defineWebSocket(hooks) { return hooks; } function defineWebSocketHandler(hooks) { return defineEventHandler({ handler() { throw createError({ statusCode: 426, statusMessage: "Upgrade Required" }); }, websocket: hooks }); } class H3Event { "__is_event__" = true; // Context node; // Node web; // Web context = {}; // Shared // Request _method; _path; _headers; _requestBody; // Response _handled = false; // Hooks _onBeforeResponseCalled; _onAfterResponseCalled; constructor(req, res) { this.node = { req, res }; } // --- Request --- get metho