import { send, getRequestHeader, getRequestHeaders, getRequestURL, getResponseHeader, setResponseHeaders, setResponseStatus } from "h3"; import { readFile } from "node:fs/promises"; import { resolve, dirname } from "node:path"; import consola from "consola"; import { ErrorParser } from "youch-core"; import { Youch } from "youch"; import { SourceMapConsumer } from "source-map"; import { defineNitroErrorHandler } from "./utils.mjs"; export default defineNitroErrorHandler( async function defaultNitroErrorHandler(error, event) { const res = await defaultHandler(error, event); if (!event.node?.res.headersSent) { setResponseHeaders(event, res.headers); } setResponseStatus(event, res.status, res.statusText); return send( event, typeof res.body === "string" ? res.body : JSON.stringify(res.body, null, 2) ); } ); export async function defaultHandler(error, event, opts) { const isSensitive = error.unhandled || error.fatal; const statusCode = error.statusCode || 500; const statusMessage = error.statusMessage || "Server Error"; const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }); if (statusCode === 404) { const baseURL = import.meta.baseURL || "/"; if (/^\/[^/]/.test(baseURL) && !url.pathname.startsWith(baseURL)) { const redirectTo = `${baseURL}${url.pathname.slice(1)}${url.search}`; return { status: 302, statusText: "Found", headers: { location: redirectTo }, body: `Redirecting...` }; } } await loadStackTrace(error).catch(consola.error); const youch = new Youch(); if (isSensitive && !opts?.silent) { const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" "); const ansiError = await (await youch.toANSI(error)).replaceAll(process.cwd(), "."); consola.error( `[request error] ${tags} [${event.method}] ${url} `, ansiError ); } const useJSON = opts?.json || !getRequestHeader(event, "accept")?.includes("text/html"); const headers = { "content-type": useJSON ? "application/json" : "text/html", // Prevent browser from guessing the MIME types of resources. "x-content-type-options": "nosniff", // Prevent error page from being embedded in an iframe "x-frame-options": "DENY", // Prevent browsers from sending the Referer header "referrer-policy": "no-referrer", // Disable the execution of any js "content-security-policy": "script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';" }; if (statusCode === 404 || !getResponseHeader(event, "cache-control")) { headers["cache-control"] = "no-cache"; } const body = useJSON ? { error: true, url, statusCode, statusMessage, message: error.message, data: error.data, stack: error.stack?.split("\n").map((line) => line.trim()) } : await youch.toHTML(error, { request: { url: url.href, method: event.method, headers: getRequestHeaders(event) } }); return { status: statusCode, statusText: statusMessage, headers, body }; } export async function loadStackTrace(error) { if (!(error instanceof Error)) { return; } const parsed = await new ErrorParser().defineSourceLoader(sourceLoader).parse(error); const stack = error.message + "\n" + parsed.frames.map((frame) => fmtFrame(frame)).join("\n"); Object.defineProperty(error, "stack", { value: stack }); if (error.cause) { await loadStackTrace(error.cause).catch(consola.error); } } async function sourceLoader(frame) { if (!frame.fileName || frame.fileType !== "fs" || frame.type === "native") { return; } if (frame.type === "app") { const rawSourceMap = await readFile(`${frame.fileName}.map`, "utf8").catch(() => { }); if (rawSourceMap) { const consumer = await new SourceMapConsumer(rawSourceMap); const originalPosition = consumer.originalPositionFor({ line: frame.lineNumber, column: frame.columnNumber }); if (originalPosition.source && originalPosition.line) { frame.fileName = resolve(dirname(frame.fileName), originalPosition.source); frame.lineNumber = originalPosition.line; frame.columnNumber = originalPosition.column || 0; } } } const contents = await readFile(frame.fileName, "utf8").catch(() => { }); return contents ? { contents } : void 0; } function fmtFrame(frame) { if (frame.type === "native") { return frame.raw; } const src = `${frame.fileName || ""}:${frame.lineNumber}:${frame.columnNumber})`; return frame.functionName ? `at ${frame.functionName} (${src}` : `at ${src}`; }