diff --git a/src/core/config/defaults.ts b/src/core/config/defaults.ts
index b69f439ed8..7dddfabbae 100644
--- a/src/core/config/defaults.ts
+++ b/src/core/config/defaults.ts
@@ -58,7 +58,7 @@ export const NitroDefaults: NitroConfig = {
baseURL: process.env.NITRO_APP_BASE_URL || "/",
handlers: [],
devHandlers: [],
- errorHandler: join(runtimeDir, "internal/error"),
+ errorHandler: join(runtimeDir, "internal/error/handler"),
routeRules: {},
prerender: {
autoSubfolderIndex: true,
diff --git a/src/core/config/resolvers/imports.ts b/src/core/config/resolvers/imports.ts
index d3e82102da..7668350e81 100644
--- a/src/core/config/resolvers/imports.ts
+++ b/src/core/config/resolvers/imports.ts
@@ -103,7 +103,7 @@ function getNitroImportsPreset(): Preset[] {
imports: ["defineTask", "runTask"],
},
{
- from: "nitropack/runtime/internal/error",
+ from: "nitropack/runtime/internal/error/utils",
imports: ["defineNitroErrorHandler"],
},
];
diff --git a/src/runtime/error.ts b/src/runtime/error.ts
index c08fefcf35..093887c87c 100644
--- a/src/runtime/error.ts
+++ b/src/runtime/error.ts
@@ -1,3 +1,121 @@
// Backward compatibility for imports from "#internal/nitro/*" or "nitropack/runtime/*"
-export { default } from "./internal/error";
+import {
+ send,
+ setResponseHeader,
+ setResponseHeaders,
+ setResponseStatus,
+} from "h3";
+import { defineNitroErrorHandler } from "./internal/error/utils";
+import { isJsonRequest, normalizeError } from "./utils";
+
+export { defineNitroErrorHandler } from "./internal/error/utils";
+
+const isDev = process.env.NODE_ENV === "development";
+
+interface ParsedError {
+ url: string;
+ statusCode: number;
+ statusMessage: number;
+ message: string;
+ stack?: string[];
+}
+
+/**
+ * @deprecated This export is only provided for backward compatibility and will be removed in v3.
+ */
+export default defineNitroErrorHandler(
+ function defaultNitroErrorHandler(error, event) {
+ const { stack, statusCode, statusMessage, message } = normalizeError(
+ error,
+ isDev
+ );
+
+ const showDetails = isDev && statusCode !== 404;
+
+ const errorObject = {
+ url: event.path || "",
+ statusCode,
+ statusMessage,
+ message,
+ stack: showDetails ? stack.map((i) => i.text) : undefined,
+ };
+
+ // Console output
+ if (error.unhandled || error.fatal) {
+ const tags = [
+ "[nitro]",
+ "[request error]",
+ error.unhandled && "[unhandled]",
+ error.fatal && "[fatal]",
+ ]
+ .filter(Boolean)
+ .join(" ");
+ console.error(
+ tags,
+ error.message + "\n" + stack.map((l) => " " + l.text).join(" \n")
+ );
+ }
+
+ if (statusCode === 404) {
+ setResponseHeader(event, "Cache-Control", "no-cache");
+ }
+
+ // Security headers
+ setResponseHeaders(event, {
+ // Disable the execution of any js
+ "Content-Security-Policy": "script-src 'none'; frame-ancestors 'none';",
+ // 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",
+ });
+
+ setResponseStatus(event, statusCode, statusMessage);
+
+ if (isJsonRequest(event)) {
+ setResponseHeader(event, "Content-Type", "application/json");
+ return send(event, JSON.stringify(errorObject));
+ }
+ setResponseHeader(event, "Content-Type", "text/html");
+ return send(event, renderHTMLError(errorObject));
+ }
+);
+
+function renderHTMLError(error: ParsedError): string {
+ const statusCode = error.statusCode || 500;
+ const statusMessage = error.statusMessage || "Request Error";
+ return `
+
+
+
+
+ ${statusCode} ${statusMessage}
+
+
+
+
+
+
+
+
+`;
+}
diff --git a/src/runtime/index.ts b/src/runtime/index.ts
index 6c3a7ef2b5..f1c9cc6b7a 100644
--- a/src/runtime/index.ts
+++ b/src/runtime/index.ts
@@ -12,7 +12,7 @@ export { useStorage } from "./internal/storage";
// Type (only) helpers
export { defineNitroPlugin } from "./internal/plugin";
export { defineRouteMeta } from "./internal/meta";
-export { defineNitroErrorHandler } from "./internal/error";
+export { defineNitroErrorHandler } from "./internal/error/utils";
// Renderer
export { defineRenderHandler } from "./internal/renderer";
diff --git a/src/runtime/internal/error.ts b/src/runtime/internal/error/handler.ts
similarity index 92%
rename from src/runtime/internal/error.ts
rename to src/runtime/internal/error/handler.ts
index d8997c5f50..039f701485 100644
--- a/src/runtime/internal/error.ts
+++ b/src/runtime/internal/error/handler.ts
@@ -1,18 +1,15 @@
-// import ansiHTML from 'ansi-html'
import {
send,
setResponseHeader,
setResponseHeaders,
setResponseStatus,
} from "h3";
-import type { NitroErrorHandler } from "nitropack/types";
-import { isJsonRequest, normalizeError } from "./utils";
-export function defineNitroErrorHandler(
- handler: NitroErrorHandler
-): NitroErrorHandler {
- return handler;
-}
+import {
+ defineNitroErrorHandler,
+ isJsonRequest,
+ normalizeError,
+} from "./utils";
const isDev = process.env.NODE_ENV === "development";
diff --git a/src/runtime/internal/error/utils.ts b/src/runtime/internal/error/utils.ts
new file mode 100644
index 0000000000..38458ae78a
--- /dev/null
+++ b/src/runtime/internal/error/utils.ts
@@ -0,0 +1,74 @@
+import type { NitroErrorHandler } from "nitropack/types";
+import type { H3Event } from "h3";
+import { getRequestHeader } from "h3";
+
+export function defineNitroErrorHandler(
+ handler: NitroErrorHandler
+): NitroErrorHandler {
+ return handler;
+}
+
+export function isJsonRequest(event: H3Event) {
+ // If the client specifically requests HTML, then avoid classifying as JSON.
+ if (hasReqHeader(event, "accept", "text/html")) {
+ return false;
+ }
+ return (
+ hasReqHeader(event, "accept", "application/json") ||
+ hasReqHeader(event, "user-agent", "curl/") ||
+ hasReqHeader(event, "user-agent", "httpie/") ||
+ hasReqHeader(event, "sec-fetch-mode", "cors") ||
+ event.path.startsWith("/api/") ||
+ event.path.endsWith(".json")
+ );
+}
+
+function hasReqHeader(event: H3Event, name: string, includes: string) {
+ const value = getRequestHeader(event, name);
+ return (
+ value && typeof value === "string" && value.toLowerCase().includes(includes)
+ );
+}
+
+export function normalizeError(error: any, isDev?: boolean) {
+ // temp fix for https://github.com/nitrojs/nitro/issues/759
+ // TODO: investigate vercel-edge not using unenv pollyfill
+ const cwd = typeof process.cwd === "function" ? process.cwd() : "/";
+
+ const stack =
+ !isDev && !import.meta.prerender && (error.unhandled || error.fatal)
+ ? []
+ : ((error.stack as string) || "")
+ .split("\n")
+ .splice(1)
+ .filter((line) => line.includes("at "))
+ .map((line) => {
+ const text = line
+ .replace(cwd + "/", "./")
+ .replace("webpack:/", "")
+ .replace("file://", "")
+ .trim();
+ return {
+ text,
+ internal:
+ (line.includes("node_modules") && !line.includes(".cache")) ||
+ line.includes("internal") ||
+ line.includes("new Promise"),
+ };
+ });
+
+ const statusCode = error.statusCode || 500;
+ const statusMessage =
+ error.statusMessage ?? (statusCode === 404 ? "Not Found" : "");
+ const message =
+ !isDev && error.unhandled
+ ? "internal server error"
+ : error.message || error.toString();
+
+ return {
+ stack,
+ statusCode,
+ statusMessage,
+ message,
+ };
+}
diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts
index a8f1e979a5..88ee171e35 100644
--- a/src/runtime/internal/utils.ts
+++ b/src/runtime/internal/utils.ts
@@ -33,71 +33,6 @@ export async function useRequestBody(
return URL.createObjectURL(blob);
}
-export function hasReqHeader(event: H3Event, name: string, includes: string) {
- const value = getRequestHeader(event, name);
- return (
- value && typeof value === "string" && value.toLowerCase().includes(includes)
- );
-}
-
-export function isJsonRequest(event: H3Event) {
- // If the client specifically requests HTML, then avoid classifying as JSON.
- if (hasReqHeader(event, "accept", "text/html")) {
- return false;
- }
- return (
- hasReqHeader(event, "accept", "application/json") ||
- hasReqHeader(event, "user-agent", "curl/") ||
- hasReqHeader(event, "user-agent", "httpie/") ||
- hasReqHeader(event, "sec-fetch-mode", "cors") ||
- event.path.startsWith("/api/") ||
- event.path.endsWith(".json")
- );
-}
-
-export function normalizeError(error: any, isDev?: boolean) {
- // temp fix for https://github.com/nitrojs/nitro/issues/759
- // TODO: investigate vercel-edge not using unenv pollyfill
- const cwd = typeof process.cwd === "function" ? process.cwd() : "/";
-
- const stack =
- !isDev && !import.meta.prerender && (error.unhandled || error.fatal)
- ? []
- : ((error.stack as string) || "")
- .split("\n")
- .splice(1)
- .filter((line) => line.includes("at "))
- .map((line) => {
- const text = line
- .replace(cwd + "/", "./")
- .replace("webpack:/", "")
- .replace("file://", "")
- .trim();
- return {
- text,
- internal:
- (line.includes("node_modules") && !line.includes(".cache")) ||
- line.includes("internal") ||
- line.includes("new Promise"),
- };
- });
-
- const statusCode = error.statusCode || 500;
- const statusMessage =
- error.statusMessage ?? (statusCode === 404 ? "Not Found" : "");
- const message =
- !isDev && error.unhandled
- ? "internal server error"
- : error.message || error.toString();
-
- return {
- stack,
- statusCode,
- statusMessage,
- message,
- };
-}
-
function _captureError(error: Error, type: string) {
console.error(`[nitro] [${type}]`, error);
useNitroApp().captureError(error, { tags: [type] });
diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts
index 2b7ecfb52c..153c191d41 100644
--- a/src/runtime/utils.ts
+++ b/src/runtime/utils.ts
@@ -1,2 +1,77 @@
// Backward compatibility for imports from "#internal/nitro/*" or "nitropack/runtime/*"
-export { isJsonRequest, normalizeError } from "./internal/utils";
+import type { H3Event } from "h3";
+import { getRequestHeader } from "h3";
+
+/**
+ * @deprecated This util is only provided for backward compatibility and will be removed in v3.
+ */
+export function isJsonRequest(event: H3Event) {
+ // If the client specifically requests HTML, then avoid classifying as JSON.
+ if (hasReqHeader(event, "accept", "text/html")) {
+ return false;
+ }
+ return (
+ hasReqHeader(event, "accept", "application/json") ||
+ hasReqHeader(event, "user-agent", "curl/") ||
+ hasReqHeader(event, "user-agent", "httpie/") ||
+ hasReqHeader(event, "sec-fetch-mode", "cors") ||
+ event.path.startsWith("/api/") ||
+ event.path.endsWith(".json")
+ );
+}
+
+/**
+ * Internal
+ */
+function hasReqHeader(event: H3Event, name: string, includes: string) {
+ const value = getRequestHeader(event, name);
+ return (
+ value && typeof value === "string" && value.toLowerCase().includes(includes)
+ );
+}
+
+/**
+ * @deprecated This util is only provided for backward compatibility and will be removed in v3.
+ */
+export function normalizeError(error: any, isDev?: boolean) {
+ // temp fix for https://github.com/nitrojs/nitro/issues/759
+ // TODO: investigate vercel-edge not using unenv pollyfill
+ const cwd = typeof process.cwd === "function" ? process.cwd() : "/";
+
+ const stack =
+ !isDev && !import.meta.prerender && (error.unhandled || error.fatal)
+ ? []
+ : ((error.stack as string) || "")
+ .split("\n")
+ .splice(1)
+ .filter((line) => line.includes("at "))
+ .map((line) => {
+ const text = line
+ .replace(cwd + "/", "./")
+ .replace("webpack:/", "")
+ .replace("file://", "")
+ .trim();
+ return {
+ text,
+ internal:
+ (line.includes("node_modules") && !line.includes(".cache")) ||
+ line.includes("internal") ||
+ line.includes("new Promise"),
+ };
+ });
+
+ const statusCode = error.statusCode || 500;
+ const statusMessage =
+ error.statusMessage ?? (statusCode === 404 ? "Not Found" : "");
+ const message =
+ !isDev && error.unhandled
+ ? "internal server error"
+ : error.message || error.toString();
+
+ return {
+ stack,
+ statusCode,
+ statusMessage,
+ message,
+ };
+}