135 lines
4.6 KiB
JavaScript
135 lines
4.6 KiB
JavaScript
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}`;
|
|
}
|