Skip to content

Commit

Permalink
refactor: split legacy error utils (#2982)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Jan 8, 2025
1 parent ff8afbd commit e8d6099
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 78 deletions.
2 changes: 1 addition & 1 deletion src/core/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/core/config/resolvers/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ function getNitroImportsPreset(): Preset[] {
imports: ["defineTask", "runTask"],
},
{
from: "nitropack/runtime/internal/error",
from: "nitropack/runtime/internal/error/utils",
imports: ["defineNitroErrorHandler"],
},
];
Expand Down
120 changes: 119 additions & 1 deletion src/runtime/error.ts
Original file line number Diff line number Diff line change
@@ -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 `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${statusCode} ${statusMessage}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico/css/pico.min.css">
</head>
<body>
<main class="container">
<dialog open>
<article>
<header>
<h2>${statusCode} ${statusMessage}</h2>
</header>
<code>
${error.message}<br><br>
${
"\n" +
(error.stack || []).map((i) => `&nbsp;&nbsp;${i}`).join("<br>")
}
</code>
<footer>
<a href="/" onclick="event.preventDefault();history.back();">Go Back</a>
</footer>
</article>
</dialog>
</main>
</body>
</html>
`;
}
2 changes: 1 addition & 1 deletion src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
74 changes: 74 additions & 0 deletions src/runtime/internal/error/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
65 changes: 0 additions & 65 deletions src/runtime/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] });
Expand Down
77 changes: 76 additions & 1 deletion src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}

0 comments on commit e8d6099

Please sign in to comment.