Spaces:
Runtime error
Runtime error
import { withoutTrailingSlash, withoutBase, getQuery as getQuery$1, decodePath, withLeadingSlash, parseURL } from 'ufo'; | |
import { parse as parse$1, serialize } from 'cookie-es'; | |
import { createRouter as createRouter$1, toRouteMatcher } from 'radix3'; | |
import destr from 'destr'; | |
import { defu } from 'defu'; | |
import crypto from 'uncrypto'; | |
import { seal, defaults, unseal } from 'iron-webcrypto'; | |
import { IncomingMessage } from 'unenv/runtime/node/http/_request'; | |
import { ServerResponse } from 'unenv/runtime/node/http/_response'; | |
function useBase(base, handler) { | |
base = 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 = withoutBase(event.path || "/", base); | |
event.node.req.url = event._path; | |
try { | |
return await handler(event); | |
} finally { | |
event._path = event.node.req.url = _path; | |
} | |
}); | |
} | |
class H3Error extends Error { | |
constructor(message, opts = {}) { | |
super(message, opts); | |
this.statusCode = 500; | |
this.fatal = false; | |
this.unhandled = false; | |
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; | |
} | |
} | |
H3Error.__h3_error__ = true; | |
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 ("stack" in input) { | |
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, | |
message: validateError.message || "Validation Failed", | |
...validateError | |
}); | |
} | |
function getQuery(event) { | |
return getQuery$1(event.path || ""); | |
} | |
function getValidatedQuery(event, validate) { | |
const query = getQuery(event); | |
return validateData(query, validate); | |
} | |
function getRouterParams(event) { | |
return event.context.params || {}; | |
} | |
function getRouterParam(event, name) { | |
const params = getRouterParams(event); | |
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 xForwardedHost = event.node.req.headers["x-forwarded-host"]; | |
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); | |
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(",")?.pop(); | |
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.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)); | |
} | |
return Buffer.from(_resolved); | |
}); | |
return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2; | |
} | |
if (!Number.parseInt(event.node.req.headers["content-length"] || "")) { | |
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 (ParsedBodySymbol in request) { | |
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; | |
} | |
return event.web?.request?.body || event._requestBody || 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(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 (key in parsedForm) { | |
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) >= opts.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 parseCookies(event) { | |
return parse$1(event.node.req.headers.cookie || ""); | |
} | |
function getCookie(event, name) { | |
return parseCookies(event)[name]; | |
} | |
function setCookie(event, name, value, serializeOptions) { | |
const cookieStr = serialize(name, value, { | |
path: "/", | |
...serializeOptions | |
}); | |
let setCookies = event.node.res.getHeader("set-cookie"); | |
if (!Array.isArray(setCookies)) { | |
setCookies = [setCookies]; | |
} | |
setCookies = setCookies.filter((cookieValue) => { | |
return cookieValue && !cookieValue.startsWith(name + "="); | |
}); | |
event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]); | |
} | |
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, cookiesString.length)); | |
} | |
} | |
return cookiesStrings; | |
} | |
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; | |
} | |
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.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 ("pipeTo" in stream) { | |
return stream.pipeTo( | |
new WritableStream({ | |
write(chunk) { | |
event.node.res.write(chunk); | |
} | |
}) | |
).then(() => { | |
event.node.res.end(); | |
}); | |
} | |
if (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 resolveCorsOptions(options = {}) { | |
const defaultOptions = { | |
origin: "*", | |
methods: "*", | |
allowHeaders: "*", | |
exposeHeaders: "*", | |
credentials: false, | |
maxAge: false, | |
preflight: { | |
statusCode: 204 | |
} | |
}; | |
return 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; | |
} | |
const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]); | |
const ignoredHeaders = /* @__PURE__ */ new Set([ | |
"transfer-encoding", | |
"connection", | |
"keep-alive", | |
"upgrade", | |
"expect", | |
"host" | |
]); | |
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), | |
opts.fetchOptions?.headers, | |
opts.headers | |
); | |
return sendProxy(event, target, { | |
...opts, | |
fetchOptions: { | |
method, | |
body, | |
duplex, | |
...opts.fetchOptions, | |
headers: fetchHeaders | |
} | |
}); | |
} | |
async function sendProxy(event, target, opts = {}) { | |
const response = await _getFetch(opts.fetch)(target, { | |
headers: opts.headers, | |
ignoreResponseError: true, | |
// make $ofetch.raw transparent | |
...opts.fetchOptions | |
}); | |
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) { | |
const headers = /* @__PURE__ */ Object.create(null); | |
const reqHeaders = getRequestHeaders(event); | |
for (const name in reqHeaders) { | |
if (!ignoredHeaders.has(name)) { | |
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), | |
...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) { | |
for (const [key, value] of Object.entries(input)) { | |
if (value !== void 0) { | |
merged.set(key, value); | |
} | |
} | |
} | |
return merged; | |
} | |
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) => { | |
await updateSession(event, config, update); | |
return sessionManager; | |
}, | |
clear: async () => { | |
await clearSession(event, config); | |
return 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); | |
} | |
if (event.context.sessions[sessionName]) { | |
return event.context.sessions[sessionName]; | |
} | |
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 = event.node.req.headers[headerName]; | |
if (typeof headerValue === "string") { | |
sealedSession = headerValue; | |
} | |
} | |
if (!sealedSession) { | |
sealedSession = getCookie(event, sessionName); | |
} | |
if (sealedSession) { | |
const unsealed = await unsealSession(event, config, sealedSession).catch( | |
() => { | |
} | |
); | |
Object.assign(session, unsealed); | |
} | |
if (!session.id) { | |
session.id = config.generateId?.() ?? (config.crypto || crypto).randomUUID(); | |
session.createdAt = Date.now(); | |
await updateSession(event, config); | |
} | |
return session; | |
} | |
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 seal(config.crypto || crypto, session, config.password, { | |
...defaults, | |
ttl: config.maxAge ? config.maxAge * 1e3 : 0, | |
...config.seal | |
}); | |
return sealed; | |
} | |
async function unsealSession(_event, config, sealed) { | |
const unsealed = await unseal( | |
config.crypto || crypto, | |
sealed, | |
config.password, | |
{ | |
...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; | |
} | |
async function clearSession(event, config) { | |
const sessionName = config.name || DEFAULT_NAME; | |
if (event.context.sessions?.[sessionName]) { | |
delete event.context.sessions[sessionName]; | |
} | |
await setCookie(event, sessionName, "", { | |
...DEFAULT_COOKIE, | |
...config.cookie | |
}); | |
} | |
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 = decodePath( | |
withLeadingSlash(withoutTrailingSlash(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({ | |
statusMessage: "Cannot find static asset " + id, | |
statusCode: 404 | |
}); | |
} | |
return false; | |
} | |
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.etag && !getResponseHeader(event, "etag")) { | |
setResponseHeader(event, "etag", meta.etag); | |
} | |
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; | |
} | |
class H3Event { | |
constructor(req, res) { | |
this["__is_event__"] = true; | |
// Web | |
this.context = {}; | |
// Response | |
this._handled = false; | |
this.node = { req, res }; | |
} | |
// --- Request --- | |
get method() { | |
if (!this._method) { | |
this._method = (this.node.req.method || "GET").toUpperCase(); | |
} | |
return this._method; | |
} | |
get path() { | |
return this._path || this.node.req.url || "/"; | |
} | |
get headers() { | |
if (!this._headers) { | |
this._headers = _normalizeNodeHeaders(this.node.req.headers); | |
} | |
return this._headers; | |
} | |
// --- Respoonse --- | |
get handled() { | |
return this._handled || this.node.res.writableEnded || this.node.res.headersSent; | |
} | |
respondWith(response) { | |
return Promise.resolve(response).then( | |
(_response) => sendWebResponse(this, _response) | |
); | |
} | |
// --- Utils --- | |
toString() { | |
return `[${this.method}] ${this.path}`; | |
} | |
toJSON() { | |
return this.toString(); | |
} | |
// --- Deprecated --- | |
/** @deprecated Please use `event.node.req` instead. **/ | |
get req() { | |
return this.node.req; | |
} | |
/** @deprecated Please use `event.node.res` instead. **/ | |
get res() { | |
return this.node.res; | |
} | |
} | |
function isEvent(input) { | |
return "__is_event__" in input; | |
} | |
function createEvent(req, res) { | |
return new H3Event(req, res); | |
} | |
function _normalizeNodeHeaders(nodeHeaders) { | |
const headers = new Headers(); | |
for (const [name, value] of Object.entries(nodeHeaders)) { | |
if (Array.isArray(value)) { | |
for (const item of value) { | |
headers.append(name, item); | |
} | |
} else if (value) { | |
headers.set(name, value); | |
} | |
} | |
return headers; | |
} | |
function defineEventHandler(handler) { | |
if (typeof handler === "function") { | |
return Object.assign(handler, { __is_handler__: true }); | |
} | |
const _hooks = { | |
onRequest: _normalizeArray(handler.onRequest), | |
onBeforeResponse: _normalizeArray(handler.onBeforeResponse) | |
}; | |
const _handler = (event) => { | |
return _callHandler(event, handler.handler, _hooks); | |
}; | |
return Object.assign(_handler, { __is_handler__: true }); | |
} | |
function _normalizeArray(input) { | |
return input ? Array.isArray(input) ? input : [input] : void 0; | |
} | |
async function _callHandler(event, handler, hooks) { | |
if (hooks.onRequest) { | |
for (const hook of hooks.onRequest) { | |
await hook(event); | |
if (event.handled) { | |
return; | |
} | |
} | |
} | |
const body = await handler(event); | |
const response = { body }; | |
if (hooks.onBeforeResponse) { | |
for (const hook of hooks.onBeforeResponse) { | |
await hook(event, response); | |
} | |
} | |
return response.body; | |
} | |
const eventHandler = defineEventHandler; | |
function defineRequestMiddleware(fn) { | |
return fn; | |
} | |
function defineResponseMiddleware(fn) { | |
return fn; | |
} | |
function isEventHandler(input) { | |
return "__is_handler__" in input; | |
} | |
function toEventHandler(input, _, _route) { | |
if (!isEventHandler(input)) { | |
console.warn( | |
"[h3] Implicit event handler conversion is deprecated. Use `eventHandler()` or `fromNodeMiddleware()` to define event handlers.", | |
_route && _route !== "/" ? ` | |
Route: ${_route}` : "", | |
` | |
Handler: ${input}` | |
); | |
} | |
return input; | |
} | |
function dynamicEventHandler(initial) { | |
let current = initial; | |
const wrapper = eventHandler((event) => { | |
if (current) { | |
return current(event); | |
} | |
}); | |
wrapper.set = (handler) => { | |
current = handler; | |
}; | |
return wrapper; | |
} | |
function defineLazyEventHandler(factory) { | |
let _promise; | |
let _resolved; | |
const resolveHandler = () => { | |
if (_resolved) { | |
return Promise.resolve(_resolved); | |
} | |
if (!_promise) { | |
_promise = Promise.resolve(factory()).then((r) => { | |
const handler = r.default || r; | |
if (typeof handler !== "function") { | |
throw new TypeError( | |
"Invalid lazy handler result. It should be a function:", | |
handler | |
); | |
} | |
_resolved = toEventHandler(r.default || r); | |
return _resolved; | |
}); | |
} | |
return _promise; | |
}; | |
return eventHandler((event) => { | |
if (_resolved) { | |
return _resolved(event); | |
} | |
return resolveHandler().then((handler) => handler(event)); | |
}); | |
} | |
const lazyEventHandler = defineLazyEventHandler; | |
class H3Headers { | |
constructor(init) { | |
if (!init) { | |
this._headers = {}; | |
} else if (Array.isArray(init)) { | |
this._headers = Object.fromEntries( | |
init.map(([key, value]) => [key.toLowerCase(), value]) | |
); | |
} else if (init && "append" in init) { | |
this._headers = Object.fromEntries(init.entries()); | |
} else { | |
this._headers = Object.fromEntries( | |
Object.entries(init).map(([key, value]) => [key.toLowerCase(), value]) | |
); | |
} | |
} | |
[Symbol.iterator]() { | |
return this.entries(); | |
} | |
entries() { | |
throw Object.entries(this._headers)[Symbol.iterator](); | |
} | |
keys() { | |
return Object.keys(this._headers)[Symbol.iterator](); | |
} | |
values() { | |
throw Object.values(this._headers)[Symbol.iterator](); | |
} | |
append(name, value) { | |
const _name = name.toLowerCase(); | |
this.set(_name, [this.get(_name), value].filter(Boolean).join(", ")); | |
} | |
delete(name) { | |
delete this._headers[name.toLowerCase()]; | |
} | |
get(name) { | |
return this._headers[name.toLowerCase()]; | |
} | |
has(name) { | |
return name.toLowerCase() in this._headers; | |
} | |
set(name, value) { | |
this._headers[name.toLowerCase()] = String(value); | |
} | |
forEach(callbackfn) { | |
for (const [key, value] of Object.entries(this._headers)) { | |
callbackfn(value, key, this); | |
} | |
} | |
} | |
class H3Response { | |
constructor(body = null, init = {}) { | |
// TODO: yet to implement | |
this.body = null; | |
this.type = "default"; | |
this.bodyUsed = false; | |
this.headers = new H3Headers(init.headers); | |
this.status = init.status ?? 200; | |
this.statusText = init.statusText || ""; | |
this.redirected = !!init.status && [301, 302, 307, 308].includes(init.status); | |
this._body = body; | |
this.url = ""; | |
this.ok = this.status < 300 && this.status > 199; | |
} | |
clone() { | |
return new H3Response(this.body, { | |
headers: this.headers, | |
status: this.status, | |
statusText: this.statusText | |
}); | |
} | |
arrayBuffer() { | |
return Promise.resolve(this._body); | |
} | |
blob() { | |
return Promise.resolve(this._body); | |
} | |
formData() { | |
return Promise.resolve(this._body); | |
} | |
json() { | |
return Promise.resolve(this._body); | |
} | |
text() { | |
return Promise.resolve(this._body); | |
} | |
} | |
function createApp(options = {}) { | |
const stack = []; | |
const handler = createAppEventHandler(stack, options); | |
const app = { | |
// @ts-ignore | |
use: (arg1, arg2, arg3) => use(app, arg1, arg2, arg3), | |
handler, | |
stack, | |
options | |
}; | |
return app; | |
} | |
function use(app, arg1, arg2, arg3) { | |
if (Array.isArray(arg1)) { | |
for (const i of arg1) { | |
use(app, i, arg2, arg3); | |
} | |
} else if (Array.isArray(arg2)) { | |
for (const i of arg2) { | |
use(app, arg1, i, arg3); | |
} | |
} else if (typeof arg1 === "string") { | |
app.stack.push( | |
normalizeLayer({ ...arg3, route: arg1, handler: arg2 }) | |
); | |
} else if (typeof arg1 === "function") { | |
app.stack.push( | |
normalizeLayer({ ...arg2, route: "/", handler: arg1 }) | |
); | |
} else { | |
app.stack.push(normalizeLayer({ ...arg1 })); | |
} | |
return app; | |
} | |
function createAppEventHandler(stack, options) { | |
const spacing = options.debug ? 2 : void 0; | |
return eventHandler(async (event) => { | |
event.node.req.originalUrl = event.node.req.originalUrl || event.node.req.url || "/"; | |
const _reqPath = event._path || event.node.req.url || "/"; | |
let _layerPath; | |
if (options.onRequest) { | |
await options.onRequest(event); | |
} | |
for (const layer of stack) { | |
if (layer.route.length > 1) { | |
if (!_reqPath.startsWith(layer.route)) { | |
continue; | |
} | |
_layerPath = _reqPath.slice(layer.route.length) || "/"; | |
} else { | |
_layerPath = _reqPath; | |
} | |
if (layer.match && !layer.match(_layerPath, event)) { | |
continue; | |
} | |
event._path = _layerPath; | |
event.node.req.url = _layerPath; | |
const val = await layer.handler(event); | |
const _body = val === void 0 ? void 0 : await val; | |
if (_body !== void 0) { | |
const _response = { body: _body }; | |
if (options.onBeforeResponse) { | |
await options.onBeforeResponse(event, _response); | |
} | |
await handleHandlerResponse(event, _response.body, spacing); | |
if (options.onAfterResponse) { | |
await options.onAfterResponse(event, _response); | |
} | |
return; | |
} | |
if (event.handled) { | |
if (options.onAfterResponse) { | |
await options.onAfterResponse(event, void 0); | |
} | |
return; | |
} | |
} | |
if (!event.handled) { | |
throw createError({ | |
statusCode: 404, | |
statusMessage: `Cannot find any path matching ${event.path || "/"}.` | |
}); | |
} | |
if (options.onAfterResponse) { | |
await options.onAfterResponse(event, void 0); | |
} | |
}); | |
} | |
function normalizeLayer(input) { | |
let handler = input.handler; | |
if (handler.handler) { | |
handler = handler.handler; | |
} | |
if (input.lazy) { | |
handler = lazyEventHandler(handler); | |
} else if (!isEventHandler(handler)) { | |
handler = toEventHandler(handler, void 0, input.route); | |
} | |
return { | |
route: withoutTrailingSlash(input.route), | |
match: input.match, | |
handler | |
}; | |
} | |
function handleHandlerResponse(event, val, jsonSpace) { | |
if (val === null) { | |
return sendNoContent(event); | |
} | |
if (val) { | |
if (isWebResponse(val)) { | |
return sendWebResponse(event, val); | |
} | |
if (isStream(val)) { | |
return sendStream(event, val); | |
} | |
if (val.buffer) { | |
return send(event, val); | |
} | |
if (val.arrayBuffer && typeof val.arrayBuffer === "function") { | |
return val.arrayBuffer().then((arrayBuffer) => { | |
return send(event, Buffer.from(arrayBuffer), val.type); | |
}); | |
} | |
if (val instanceof Error) { | |
throw createError(val); | |
} | |
if (typeof val.end === "function") { | |
return true; | |
} | |
} | |
const valType = typeof val; | |
if (valType === "string") { | |
return send(event, val, MIMES.html); | |
} | |
if (valType === "object" || valType === "boolean" || valType === "number") { | |
return send(event, JSON.stringify(val, void 0, jsonSpace), MIMES.json); | |
} | |
if (valType === "bigint") { | |
return send(event, val.toString(), MIMES.json); | |
} | |
throw createError({ | |
statusCode: 500, | |
statusMessage: `[h3] Cannot send ${valType} as response.` | |
}); | |
} | |
const RouterMethods = [ | |
"connect", | |
"delete", | |
"get", | |
"head", | |
"options", | |
"post", | |
"put", | |
"trace", | |
"patch" | |
]; | |
function createRouter(opts = {}) { | |
const _router = createRouter$1({}); | |
const routes = {}; | |
let _matcher; | |
const router = {}; | |
const addRoute = (path, handler, method) => { | |
let route = routes[path]; | |
if (!route) { | |
routes[path] = route = { path, handlers: {} }; | |
_router.insert(path, route); | |
} | |
if (Array.isArray(method)) { | |
for (const m of method) { | |
addRoute(path, handler, m); | |
} | |
} else { | |
route.handlers[method] = toEventHandler(handler, void 0, path); | |
} | |
return router; | |
}; | |
router.use = router.add = (path, handler, method) => addRoute(path, handler, method || "all"); | |
for (const method of RouterMethods) { | |
router[method] = (path, handle) => router.add(path, handle, method); | |
} | |
router.handler = eventHandler((event) => { | |
let path = event.path || "/"; | |
const qIndex = path.indexOf("?"); | |
if (qIndex !== -1) { | |
path = path.slice(0, Math.max(0, qIndex)); | |
} | |
const matched = _router.lookup(path); | |
if (!matched || !matched.handlers) { | |
if (opts.preemptive || opts.preemtive) { | |
throw createError({ | |
statusCode: 404, | |
name: "Not Found", | |
statusMessage: `Cannot find any route matching ${event.path || "/"}.` | |
}); | |
} else { | |
return; | |
} | |
} | |
const method = (event.node.req.method || "get").toLowerCase(); | |
let handler = matched.handlers[method] || matched.handlers.all; | |
if (!handler) { | |
if (!_matcher) { | |
_matcher = toRouteMatcher(_router); | |
} | |
const _matches = _matcher.matchAll(path).reverse(); | |
for (const _match of _matches) { | |
if (_match.handlers[method]) { | |
handler = _match.handlers[method]; | |
matched.handlers[method] = matched.handlers[method] || handler; | |
break; | |
} | |
if (_match.handlers.all) { | |
handler = _match.handlers.all; | |
matched.handlers.all = matched.handlers.all || handler; | |
break; | |
} | |
} | |
} | |
if (!handler) { | |
if (opts.preemptive || opts.preemtive) { | |
throw createError({ | |
statusCode: 405, | |
name: "Method Not Allowed", | |
statusMessage: `Method ${method} is not allowed on this route.` | |
}); | |
} else { | |
return; | |
} | |
} | |
event.context.matchedRoute = matched; | |
const params = matched.params || {}; | |
event.context.params = params; | |
return Promise.resolve(handler(event)).then((res) => { | |
if (res === void 0 && (opts.preemptive || opts.preemtive)) { | |
return null; | |
} | |
return res; | |
}); | |
}); | |
return router; | |
} | |
const defineNodeListener = (handler) => handler; | |
const defineNodeMiddleware = (middleware) => middleware; | |
function fromNodeMiddleware(handler) { | |
if (isEventHandler(handler)) { | |
return handler; | |
} | |
if (typeof handler !== "function") { | |
throw new TypeError( | |
"Invalid handler. It should be a function:", | |
handler | |
); | |
} | |
return eventHandler((event) => { | |
return callNodeListener( | |
handler, | |
event.node.req, | |
event.node.res | |
); | |
}); | |
} | |
function toNodeListener(app) { | |
const toNodeHandle = async function(req, res) { | |
const event = createEvent(req, res); | |
try { | |
await app.handler(event); | |
} catch (_error) { | |
const error = createError(_error); | |
if (!isError(_error)) { | |
error.unhandled = true; | |
} | |
if (app.options.onError) { | |
await app.options.onError(error, event); | |
} | |
if (event.handled) { | |
return; | |
} | |
if (error.unhandled || error.fatal) { | |
console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); | |
} | |
await sendError(event, error, !!app.options.debug); | |
} | |
}; | |
return toNodeHandle; | |
} | |
function promisifyNodeListener(handler) { | |
return function(req, res) { | |
return callNodeListener(handler, req, res); | |
}; | |
} | |
function callNodeListener(handler, req, res) { | |
const isMiddleware = handler.length > 2; | |
return new Promise((resolve, reject) => { | |
const next = (err) => { | |
if (isMiddleware) { | |
res.off("close", next); | |
res.off("error", next); | |
} | |
return err ? reject(createError(err)) : resolve(void 0); | |
}; | |
try { | |
const returned = handler(req, res, next); | |
if (isMiddleware && returned === void 0) { | |
res.once("close", next); | |
res.once("error", next); | |
} else { | |
resolve(returned); | |
} | |
} catch (error) { | |
next(error); | |
} | |
}); | |
} | |
function toPlainHandler(app) { | |
const handler = (request) => { | |
return _handlePlainRequest(app, request); | |
}; | |
return handler; | |
} | |
function fromPlainHandler(handler) { | |
return eventHandler(async (event) => { | |
const res = await handler({ | |
method: event.method, | |
path: event.path, | |
headers: Object.fromEntries(event.headers.entries()), | |
body: getRequestWebStream(event), | |
context: event.context | |
}); | |
setResponseStatus(event, res.status, res.statusText); | |
for (const [key, value] of res.headers) { | |
setResponseHeader(event, key, value); | |
} | |
return res.body; | |
}); | |
} | |
async function _handlePlainRequest(app, request) { | |
const path = request.path; | |
const method = (request.method || "GET").toUpperCase(); | |
const headers = new Headers(request.headers); | |
const nodeReq = new IncomingMessage(); | |
const nodeRes = new ServerResponse(nodeReq); | |
nodeReq.method = method; | |
nodeReq.url = path; | |
nodeReq.headers = Object.fromEntries(headers.entries()); | |
const event = createEvent(nodeReq, nodeRes); | |
event._method = method; | |
event._path = path; | |
event._headers = headers; | |
if (request.body) { | |
event._requestBody = request.body; | |
} | |
if (request._eventOverrides) { | |
Object.assign(event, request._eventOverrides); | |
} | |
if (request.context) { | |
Object.assign(event.context, request.context); | |
} | |
try { | |
await app.handler(event); | |
} catch (_error) { | |
const error = createError(_error); | |
if (!isError(_error)) { | |
error.unhandled = true; | |
} | |
if (app.options.onError) { | |
await app.options.onError(error, event); | |
} | |
if (!event.handled) { | |
if (error.unhandled || error.fatal) { | |
console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); | |
} | |
await sendError(event, error, !!app.options.debug); | |
} | |
} | |
return { | |
status: nodeRes.statusCode, | |
statusText: nodeRes.statusMessage, | |
headers: _normalizeUnenvHeaders(nodeRes._headers), | |
body: nodeRes._data | |
}; | |
} | |
function _normalizeUnenvHeaders(input) { | |
const headers = []; | |
const cookies = []; | |
for (const _key in input) { | |
const key = _key.toLowerCase(); | |
if (key === "set-cookie") { | |
cookies.push( | |
...splitCookiesString(input["set-cookie"]) | |
); | |
continue; | |
} | |
const value = input[key]; | |
if (Array.isArray(value)) { | |
for (const _value of value) { | |
headers.push([key, _value]); | |
} | |
} else if (value !== void 0) { | |
headers.push([key, String(value)]); | |
} | |
} | |
if (cookies.length > 0) { | |
for (const cookie of cookies) { | |
headers.push(["set-cookie", cookie]); | |
} | |
} | |
return headers; | |
} | |
function toWebHandler(app) { | |
const webHandler = (request, context) => { | |
return _handleWebRequest(app, request, context); | |
}; | |
return webHandler; | |
} | |
function fromWebHandler(handler) { | |
return eventHandler((event) => handler(toWebRequest(event), event.context)); | |
} | |
const nullBodyResponses = /* @__PURE__ */ new Set([101, 204, 205, 304]); | |
async function _handleWebRequest(app, request, context) { | |
const url = new URL(request.url); | |
const res = await _handlePlainRequest(app, { | |
_eventOverrides: { | |
web: { request, url } | |
}, | |
context, | |
method: request.method, | |
path: url.pathname + url.search, | |
headers: request.headers, | |
body: request.body | |
}); | |
const body = nullBodyResponses.has(res.status) || request.method === "HEAD" ? null : res.body; | |
return new Response(body, { | |
status: res.status, | |
statusText: res.statusText, | |
headers: res.headers | |
}); | |
} | |
export { H3Error, H3Event, H3Headers, H3Response, MIMES, appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearResponseHeaders, clearSession, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, defineRequestMiddleware, defineResponseMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, fromPlainHandler, fromWebHandler, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestHeader, getRequestHeaders, getRequestHost, getRequestIP, getRequestPath, getRequestProtocol, getRequestURL, getRequestWebStream, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, getValidatedQuery, handleCacheHeaders, handleCors, isCorsOriginAllowed, isError, isEvent, isEventHandler, isMethod, isPreflightRequest, isStream, isWebResponse, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readFormData, readMultipartFormData, readRawBody, readValidatedBody, removeResponseHeader, sanitizeStatusCode, sanitizeStatusMessage, sealSession, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, sendWebResponse, serveStatic, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, splitCookiesString, toEventHandler, toNodeListener, toPlainHandler, toWebHandler, toWebRequest, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints }; | |