-
Notifications
You must be signed in to change notification settings - Fork 540
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
277 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) => ` ${i}`).join("<br>") | ||
} | ||
</code> | ||
<footer> | ||
<a href="/" onclick="event.preventDefault();history.back();">Go Back</a> | ||
</footer> | ||
</article> | ||
</dialog> | ||
</main> | ||
</body> | ||
</html> | ||
`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 5 additions & 8 deletions
13
src/runtime/internal/error.ts → src/runtime/internal/error/handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |