From 2f91f24570e0035f27ca3a0f23218113c8f333b3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 17:29:03 +0100 Subject: [PATCH 01/16] feat: better errors --- package.json | 3 +- pnpm-lock.yaml | 80 +++++++++++++++++ src/core/config/defaults.ts | 4 +- src/core/config/loader.ts | 2 + src/core/config/resolvers/error.ts | 13 +++ src/runtime/internal/error/dev.ts | 75 ++++++++++++++++ src/runtime/internal/error/handler.ts | 118 -------------------------- src/runtime/internal/error/prod.ts | 46 ++++++++++ src/runtime/internal/error/utils.ts | 60 ++++--------- 9 files changed, 236 insertions(+), 165 deletions(-) create mode 100644 src/core/config/resolvers/error.ts create mode 100644 src/runtime/internal/error/dev.ts delete mode 100644 src/runtime/internal/error/handler.ts create mode 100644 src/runtime/internal/error/prod.ts diff --git a/package.json b/package.json index 3e0f5160d6..d73212ecdd 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,8 @@ "unimport": "^3.14.5", "unstorage": "^1.14.4", "untyped": "^1.5.2", - "unwasm": "^0.3.9" + "unwasm": "^0.3.9", + "youch": "4.1.0-beta.4" }, "devDependencies": { "@azure/functions": "^3.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14e62bc1bc..dffc90c20a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,6 +216,9 @@ importers: unwasm: specifier: ^0.3.9 version: 0.3.9 + youch: + specifier: 4.1.0-beta.4 + version: 4.1.0-beta.4 devDependencies: '@azure/functions': specifier: ^3.5.1 @@ -1319,6 +1322,17 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@poppinss/colors@4.1.4': + resolution: {integrity: sha512-FA+nTU8p6OcSH4tLDY5JilGYr1bVWHpNmcLr7xmMEdbWmKHa+3QZ+DqefrXKmdjO/brHTnQZo20lLSjaO7ydog==} + engines: {node: '>=18.16.0'} + + '@poppinss/dumper@0.6.2': + resolution: {integrity: sha512-FhE9rY15aZ6Qp6ltQ0NZjseVRhwgWZ7+sg16343FqnjdUQvvBBi5eSeH/aZA4LF1ZOV5779DYrJXTHT42JlHNg==} + + '@poppinss/exception@1.2.0': + resolution: {integrity: sha512-WLneXKQYNClhaMXccO111VQmZahSrcSRDaHRbV6KL5R4pTvK87fMn/MXLUcvOjk0X5dTHDPKF61tM7j826wrjQ==} + engines: {node: '>=20.6.0'} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1625,6 +1639,10 @@ packages: resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} engines: {node: '>=6'} + '@sindresorhus/is@7.0.1': + resolution: {integrity: sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==} + engines: {node: '>=18'} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -1633,6 +1651,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@speed-highlight/core@1.2.7': + resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2552,6 +2573,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + core-js-compat@3.40.0: resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} @@ -2888,6 +2913,9 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-stack-parser-es@0.1.5: + resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -3866,6 +3894,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + klona@2.0.6: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} @@ -5405,6 +5437,10 @@ packages: resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + supports-color@10.0.0: + resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -6055,9 +6091,17 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} + youch-core@0.3.1: + resolution: {integrity: sha512-KOAmtABz17fgK+uBBJYIzaPpIgX+JgTRgY4t3zXH18akc5rRtFkRmcNTMCuSxLdbOJDY9+T/O3nyA/EQuN4EWA==} + engines: {node: '>=20.6.0'} + youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + youch@4.1.0-beta.4: + resolution: {integrity: sha512-sknU8tgzmqx7PJBEqAdqBYjleLjHfNnFBTi3HwQTh6I+4OoJaIHkezxxUJ/ta7cpfgdG87bdYPCucVZV+GzhRA==} + engines: {node: '>=20.6.0'} + zhead@2.2.4: resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} @@ -7080,6 +7124,18 @@ snapshots: '@popperjs/core@2.11.8': {} + '@poppinss/colors@4.1.4': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.2': + dependencies: + '@poppinss/colors': 4.1.4 + '@sindresorhus/is': 7.0.1 + supports-color: 10.0.0 + + '@poppinss/exception@1.2.0': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -7524,10 +7580,14 @@ snapshots: '@sindresorhus/is@0.14.0': {} + '@sindresorhus/is@7.0.1': {} + '@sindresorhus/merge-streams@2.3.0': {} '@sindresorhus/merge-streams@4.0.0': {} + '@speed-highlight/core@1.2.7': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -8619,6 +8679,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.0.2: {} + core-js-compat@3.40.0: dependencies: browserslist: 4.24.4 @@ -8950,6 +9012,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@0.1.5: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -10170,6 +10234,8 @@ snapshots: kleur@3.0.3: {} + kleur@4.1.5: {} + klona@2.0.6: {} knitwork@1.2.0: {} @@ -11939,6 +12005,8 @@ snapshots: sudo-prompt@8.2.5: {} + supports-color@10.0.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -12639,12 +12707,24 @@ snapshots: yoctocolors@2.1.1: {} + youch-core@0.3.1: + dependencies: + '@poppinss/exception': 1.2.0 + error-stack-parser-es: 0.1.5 + youch@3.3.4: dependencies: cookie: 0.7.2 mustache: 4.2.0 stacktracey: 2.1.8 + youch@4.1.0-beta.4: + dependencies: + '@poppinss/dumper': 0.6.2 + '@speed-highlight/core': 1.2.7 + cookie: 1.0.2 + youch-core: 0.3.1 + zhead@2.2.4: {} zip-stream@6.0.1: diff --git a/src/core/config/defaults.ts b/src/core/config/defaults.ts index 7dddfabbae..00d0e1a6dd 100644 --- a/src/core/config/defaults.ts +++ b/src/core/config/defaults.ts @@ -1,6 +1,6 @@ import { runtimeDir } from "nitropack/runtime/meta"; import type { NitroConfig } from "nitropack/types"; -import { join, resolve } from "pathe"; +import { resolve } from "pathe"; import { isDebug, isTest } from "std-env"; export const NitroDefaults: NitroConfig = { @@ -58,7 +58,7 @@ export const NitroDefaults: NitroConfig = { baseURL: process.env.NITRO_APP_BASE_URL || "/", handlers: [], devHandlers: [], - errorHandler: join(runtimeDir, "internal/error/handler"), + errorHandler: undefined, routeRules: {}, prerender: { autoSubfolderIndex: true, diff --git a/src/core/config/loader.ts b/src/core/config/loader.ts index bd87cf5d15..b8674b9898 100644 --- a/src/core/config/loader.ts +++ b/src/core/config/loader.ts @@ -27,6 +27,7 @@ import { resolveRouteRulesOptions } from "./resolvers/route-rules"; import { resolveRuntimeConfigOptions } from "./resolvers/runtime-config"; import { resolveStorageOptions } from "./resolvers/storage"; import { resolveURLOptions } from "./resolvers/url"; +import { resolveErrorOptions } from "./resolvers/error"; const configResolvers = [ resolveCompatibilityOptions, @@ -41,6 +42,7 @@ const configResolvers = [ resolveURLOptions, resolveAssetsOptions, resolveStorageOptions, + resolveErrorOptions, ] as const; export async function loadOptions( diff --git a/src/core/config/resolvers/error.ts b/src/core/config/resolvers/error.ts new file mode 100644 index 0000000000..675ee69666 --- /dev/null +++ b/src/core/config/resolvers/error.ts @@ -0,0 +1,13 @@ +import _consola from "consola"; +import { runtimeDir } from "nitropack/runtime/meta"; +import type { NitroOptions } from "nitropack/types"; +import { join } from "pathe"; + +export async function resolveErrorOptions(options: NitroOptions) { + if (!options.errorHandler) { + options.errorHandler = join( + runtimeDir, + `internal/error/${options.dev ? "dev" : "prod"}` + ); + } +} diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts new file mode 100644 index 0000000000..c8d7b010e6 --- /dev/null +++ b/src/runtime/internal/error/dev.ts @@ -0,0 +1,75 @@ +import { + send, + getRequestHeaders, + setResponseHeader, + setResponseStatus, + getRequestURL, +} from "h3"; + +import consola from "consola"; +import { Youch } from "youch"; + +import { + defineNitroErrorHandler, + isJsonRequest, + setSecurityHeaders, +} from "./utils"; + +export default defineNitroErrorHandler( + async function defaultNitroErrorHandler(error, event) { + const statusCode = error.statusCode || 500; + const statusMessage = error.statusMessage || "Internal Server Error"; + // prettier-ignore + const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); + + // 404 + if (statusCode === 404) { + setResponseHeader(event, "Cache-Control", "no-cache"); + } + + const youch = new Youch(); + + // Console output + if (error.unhandled || error.fatal) { + // prettier-ignore + const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" ") + // const ansiError = await youch.toANSI(error); + consola.error( + `[nitro] [request error] ${tags} [${event.method}] ${url}\n`, + error + ); + } + + // Send response + setResponseStatus(event, statusCode, statusMessage); + setSecurityHeaders(event, true /* allow js */); + return isJsonRequest(event) + ? send( + event, + JSON.stringify( + { + stack: error.stack?.split("\n").map((line) => line.trim()), + url, + statusCode, + statusMessage, + message: error.message, + data: error.data, + }, + null, + 2 + ), + "application/json" + ) + : send( + event, + await youch.toHTML(error, { + request: { + url, + method: event.method, + headers: getRequestHeaders(event), + }, + }), + "text/html" + ); + } +); diff --git a/src/runtime/internal/error/handler.ts b/src/runtime/internal/error/handler.ts deleted file mode 100644 index 039f701485..0000000000 --- a/src/runtime/internal/error/handler.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - send, - setResponseHeader, - setResponseHeaders, - setResponseStatus, -} from "h3"; - -import { - defineNitroErrorHandler, - isJsonRequest, - normalizeError, -} from "./utils"; - -const isDev = process.env.NODE_ENV === "development"; - -interface ParsedError { - url: string; - statusCode: number; - statusMessage: number; - message: string; - stack?: string[]; -} - -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} - - - -
- -
-
-

${statusCode} ${statusMessage}

-
- - ${error.message}

- ${ - "\n" + - (error.stack || []).map((i) => `  ${i}`).join("
") - } -
- -
-
-
- - -`; -} diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts new file mode 100644 index 0000000000..2facda5bdf --- /dev/null +++ b/src/runtime/internal/error/prod.ts @@ -0,0 +1,46 @@ +import { getRequestURL, send, setResponseHeader, setResponseStatus } from "h3"; +import { defineNitroErrorHandler, setSecurityHeaders } from "./utils"; + +export default defineNitroErrorHandler( + function defaultNitroErrorHandler(error, event) { + const statusCode = error.statusCode || 500; + const statusMessage = error.statusMessage || "Internal Server Error"; + // prettier-ignore + const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); + + // 404 + if (statusCode === 404) { + setResponseHeader(event, "Cache-Control", "no-cache"); + return send(event, ""); + } + + // Console output + if (error.unhandled || error.fatal) { + // prettier-ignore + const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" ") + console.error( + `[nitro] [request error] ${tags} [${event.method}] ${event.path}\n`, + error + ); + } + + // Send response + setSecurityHeaders(event, false /* no js */); + setResponseStatus(event, statusCode, statusMessage); + return send( + event, + JSON.stringify( + { + url, + statusCode, + statusMessage, + message: "server error", + data: error.data, + }, + null, + 2 + ), + "application/json" + ); + } +); diff --git a/src/runtime/internal/error/utils.ts b/src/runtime/internal/error/utils.ts index 38458ae78a..18a07b5ce3 100644 --- a/src/runtime/internal/error/utils.ts +++ b/src/runtime/internal/error/utils.ts @@ -1,6 +1,6 @@ import type { NitroErrorHandler } from "nitropack/types"; import type { H3Event } from "h3"; -import { getRequestHeader } from "h3"; +import { getRequestHeader, setResponseHeaders } from "h3"; export function defineNitroErrorHandler( handler: NitroErrorHandler @@ -23,52 +23,24 @@ export function isJsonRequest(event: H3Event) { ); } +export function setSecurityHeaders(event: H3Event, allowjs = false) { + setResponseHeaders(event, { + // 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": allowjs + ? "script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';" + : "script-src 'none'; frame-ancestors 'none';", + }); +} + 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, - }; -} From 73f6b5dc56158c4f9520ebd22c55f1de28a9df6e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 17:46:06 +0100 Subject: [PATCH 02/16] full url in prod --- src/runtime/internal/error/prod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts index 2facda5bdf..b26467bd39 100644 --- a/src/runtime/internal/error/prod.ts +++ b/src/runtime/internal/error/prod.ts @@ -19,7 +19,7 @@ export default defineNitroErrorHandler( // prettier-ignore const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" ") console.error( - `[nitro] [request error] ${tags} [${event.method}] ${event.path}\n`, + `[nitro] [request error] ${tags} [${event.method}] ${url}\n`, error ); } From 0a7f0d8c5292a728ba9bb9c3c0c5734b14661c18 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 17:48:03 +0100 Subject: [PATCH 03/16] update test --- test/tests.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/tests.ts b/test/tests.ts index 5a6663efb2..b9c9052b82 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -384,9 +384,12 @@ export function testNitro( }, }); expect(status).toBe(503); + expect(headers).toMatchObject({ "content-type": "application/json", - "content-security-policy": "script-src 'none'; frame-ancestors 'none';", + "content-security-policy": ctx.isDev + ? "script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';" + : "script-src 'none'; frame-ancestors 'none';", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "x-frame-options": "DENY", From b9a759029505dc21a766c58342476dacb8e436ba Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 18:02:58 +0100 Subject: [PATCH 04/16] remove unused import --- src/core/config/resolvers/error.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/config/resolvers/error.ts b/src/core/config/resolvers/error.ts index 675ee69666..3ea54ff672 100644 --- a/src/core/config/resolvers/error.ts +++ b/src/core/config/resolvers/error.ts @@ -1,4 +1,3 @@ -import _consola from "consola"; import { runtimeDir } from "nitropack/runtime/meta"; import type { NitroOptions } from "nitropack/types"; import { join } from "pathe"; From 9fc7b6cffa496d7bc0e72b63ce936730cfd7298d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 22:46:31 +0100 Subject: [PATCH 05/16] include prod message if non fatal/unhandled --- src/runtime/internal/error/prod.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts index b26467bd39..f980053579 100644 --- a/src/runtime/internal/error/prod.ts +++ b/src/runtime/internal/error/prod.ts @@ -34,7 +34,8 @@ export default defineNitroErrorHandler( url, statusCode, statusMessage, - message: "server error", + message: + error.unhandled || error.fatal ? "server error" : error.message, data: error.data, }, null, From 1d6f91e35270b4d670e4da1f668f8b691e98adb1 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 22:50:51 +0100 Subject: [PATCH 06/16] use same obj for 404 --- src/runtime/internal/error/dev.ts | 11 +++++------ src/runtime/internal/error/prod.ts | 10 ++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index c8d7b010e6..984e570034 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -22,11 +22,6 @@ export default defineNitroErrorHandler( // prettier-ignore const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); - // 404 - if (statusCode === 404) { - setResponseHeader(event, "Cache-Control", "no-cache"); - } - const youch = new Youch(); // Console output @@ -43,17 +38,21 @@ export default defineNitroErrorHandler( // Send response setResponseStatus(event, statusCode, statusMessage); setSecurityHeaders(event, true /* allow js */); + if (statusCode === 404) { + setResponseHeader(event, "Cache-Control", "no-cache"); + } return isJsonRequest(event) ? send( event, JSON.stringify( { - stack: error.stack?.split("\n").map((line) => line.trim()), + error: true, url, statusCode, statusMessage, message: error.message, data: error.data, + stack: error.stack?.split("\n").map((line) => line.trim()), }, null, 2 diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts index f980053579..a48a37bdd4 100644 --- a/src/runtime/internal/error/prod.ts +++ b/src/runtime/internal/error/prod.ts @@ -8,12 +8,6 @@ export default defineNitroErrorHandler( // prettier-ignore const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); - // 404 - if (statusCode === 404) { - setResponseHeader(event, "Cache-Control", "no-cache"); - return send(event, ""); - } - // Console output if (error.unhandled || error.fatal) { // prettier-ignore @@ -27,10 +21,14 @@ export default defineNitroErrorHandler( // Send response setSecurityHeaders(event, false /* no js */); setResponseStatus(event, statusCode, statusMessage); + if (statusCode === 404) { + setResponseHeader(event, "Cache-Control", "no-cache"); + } return send( event, JSON.stringify( { + error: true, url, statusCode, statusMessage, From 2d5f54c9ffa141922ca842a4285189c15ddc502b Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 22:57:15 +0100 Subject: [PATCH 07/16] simplify json/html: only html if requested --- src/runtime/internal/error/dev.ts | 31 +++++++++++++---------------- src/runtime/internal/error/utils.ts | 15 -------------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index 984e570034..4952d7c8ae 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -1,5 +1,6 @@ import { send, + getRequestHeader, getRequestHeaders, setResponseHeader, setResponseStatus, @@ -9,11 +10,7 @@ import { import consola from "consola"; import { Youch } from "youch"; -import { - defineNitroErrorHandler, - isJsonRequest, - setSecurityHeaders, -} from "./utils"; +import { defineNitroErrorHandler, setSecurityHeaders } from "./utils"; export default defineNitroErrorHandler( async function defaultNitroErrorHandler(error, event) { @@ -41,8 +38,19 @@ export default defineNitroErrorHandler( if (statusCode === 404) { setResponseHeader(event, "Cache-Control", "no-cache"); } - return isJsonRequest(event) + return getRequestHeader(event, "accept")?.includes("text/html") ? send( + event, + await youch.toHTML(error, { + request: { + url, + method: event.method, + headers: getRequestHeaders(event), + }, + }), + "text/html" + ) + : send( event, JSON.stringify( { @@ -58,17 +66,6 @@ export default defineNitroErrorHandler( 2 ), "application/json" - ) - : send( - event, - await youch.toHTML(error, { - request: { - url, - method: event.method, - headers: getRequestHeaders(event), - }, - }), - "text/html" ); } ); diff --git a/src/runtime/internal/error/utils.ts b/src/runtime/internal/error/utils.ts index 18a07b5ce3..b521ff8af8 100644 --- a/src/runtime/internal/error/utils.ts +++ b/src/runtime/internal/error/utils.ts @@ -8,21 +8,6 @@ export function defineNitroErrorHandler( 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") - ); -} - export function setSecurityHeaders(event: H3Event, allowjs = false) { setResponseHeaders(event, { // Prevent browser from guessing the MIME types of resources. From a70571a95f0ffa6d0559565cdb9139fb92bc920d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 23:00:58 +0100 Subject: [PATCH 08/16] consistent default message --- playground/routes/index.ts | 2 +- src/runtime/internal/error/dev.ts | 2 +- src/runtime/internal/error/prod.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/playground/routes/index.ts b/playground/routes/index.ts index fecf90a83a..fdbeabc68f 100644 --- a/playground/routes/index.ts +++ b/playground/routes/index.ts @@ -1,3 +1,3 @@ export default eventHandler(async (event) => { - return {}; + throw createError("foo"); }); diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index 4952d7c8ae..3b08d2e48b 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -15,7 +15,7 @@ import { defineNitroErrorHandler, setSecurityHeaders } from "./utils"; export default defineNitroErrorHandler( async function defaultNitroErrorHandler(error, event) { const statusCode = error.statusCode || 500; - const statusMessage = error.statusMessage || "Internal Server Error"; + const statusMessage = error.statusMessage || "Server Error"; // prettier-ignore const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts index a48a37bdd4..80870732a1 100644 --- a/src/runtime/internal/error/prod.ts +++ b/src/runtime/internal/error/prod.ts @@ -4,7 +4,7 @@ import { defineNitroErrorHandler, setSecurityHeaders } from "./utils"; export default defineNitroErrorHandler( function defaultNitroErrorHandler(error, event) { const statusCode = error.statusCode || 500; - const statusMessage = error.statusMessage || "Internal Server Error"; + const statusMessage = error.statusMessage || "Server Error"; // prettier-ignore const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); @@ -33,7 +33,7 @@ export default defineNitroErrorHandler( statusCode, statusMessage, message: - error.unhandled || error.fatal ? "server error" : error.message, + error.unhandled || error.fatal ? "Server Error" : error.message, data: error.data, }, null, From 21394594c0a1d892941c8c2f5e0b8857a51d12be Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 23:01:20 +0100 Subject: [PATCH 09/16] revert playground --- playground/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/routes/index.ts b/playground/routes/index.ts index fdbeabc68f..fecf90a83a 100644 --- a/playground/routes/index.ts +++ b/playground/routes/index.ts @@ -1,3 +1,3 @@ export default eventHandler(async (event) => { - throw createError("foo"); + return {}; }); From e31435c8ead2cb333788f67bb3ffe71657457f94 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 23:03:41 +0100 Subject: [PATCH 10/16] always add default cache-control --- src/runtime/internal/error/dev.ts | 5 +++-- src/runtime/internal/error/prod.ts | 13 ++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index 3b08d2e48b..7184ada568 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -5,6 +5,7 @@ import { setResponseHeader, setResponseStatus, getRequestURL, + getResponseHeader, } from "h3"; import consola from "consola"; @@ -35,8 +36,8 @@ export default defineNitroErrorHandler( // Send response setResponseStatus(event, statusCode, statusMessage); setSecurityHeaders(event, true /* allow js */); - if (statusCode === 404) { - setResponseHeader(event, "Cache-Control", "no-cache"); + if (statusCode === 404 || !getResponseHeader(event, "cache-control")) { + setResponseHeader(event, "cache-control", "no-cache"); } return getRequestHeader(event, "accept")?.includes("text/html") ? send( diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts index 80870732a1..647aed1940 100644 --- a/src/runtime/internal/error/prod.ts +++ b/src/runtime/internal/error/prod.ts @@ -1,4 +1,11 @@ -import { getRequestURL, send, setResponseHeader, setResponseStatus } from "h3"; +import { + getRequestHeader, + getRequestURL, + getResponseHeader, + send, + setResponseHeader, + setResponseStatus, +} from "h3"; import { defineNitroErrorHandler, setSecurityHeaders } from "./utils"; export default defineNitroErrorHandler( @@ -21,8 +28,8 @@ export default defineNitroErrorHandler( // Send response setSecurityHeaders(event, false /* no js */); setResponseStatus(event, statusCode, statusMessage); - if (statusCode === 404) { - setResponseHeader(event, "Cache-Control", "no-cache"); + if (statusCode === 404 || !getResponseHeader(event, "cache-control")) { + setResponseHeader(event, "cache-control", "no-cache"); } return send( event, From 2fb1520fd776b7f1f56022e15a2a8f32c2ac78c5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 14 Jan 2025 23:55:46 +0100 Subject: [PATCH 11/16] built-in sourcemap support --- package.json | 4 +- pnpm-lock.yaml | 6 +++ src/runtime/internal/error/dev.ts | 68 +++++++++++++++++++++++++++--- src/runtime/internal/error/prod.ts | 1 - 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d73212ecdd..80a2749514 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "semver": "^7.6.3", "serve-placeholder": "^2.0.2", "serve-static": "^1.16.2", + "source-map": "^0.7.4", "std-env": "^3.8.0", "ufo": "^1.5.4", "uncrypto": "^0.1.3", @@ -165,7 +166,8 @@ "unstorage": "^1.14.4", "untyped": "^1.5.2", "unwasm": "^0.3.9", - "youch": "4.1.0-beta.4" + "youch": "4.1.0-beta.4", + "youch-core": "^0.3.1" }, "devDependencies": { "@azure/functions": "^3.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dffc90c20a..6a39110a6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,6 +189,9 @@ importers: serve-static: specifier: ^1.16.2 version: 1.16.2 + source-map: + specifier: ^0.7.4 + version: 0.7.4 std-env: specifier: ^3.8.0 version: 3.8.0 @@ -219,6 +222,9 @@ importers: youch: specifier: 4.1.0-beta.4 version: 4.1.0-beta.4 + youch-core: + specifier: ^0.3.1 + version: 0.3.1 devDependencies: '@azure/functions': specifier: ^3.5.1 diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index 7184ada568..a9b0935a7e 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -7,10 +7,12 @@ import { getRequestURL, getResponseHeader, } 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, setSecurityHeaders } from "./utils"; export default defineNitroErrorHandler( @@ -20,19 +22,22 @@ export default defineNitroErrorHandler( // prettier-ignore const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); - const youch = new Youch(); + // Load stack trace with source maps + await loadStackTrace(error); // Console output if (error.unhandled || error.fatal) { // prettier-ignore const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" ") - // const ansiError = await youch.toANSI(error); consola.error( - `[nitro] [request error] ${tags} [${event.method}] ${url}\n`, + `[nitro] [request error] ${tags} [${event.method}] ${url}\n\n`, error ); } + // https://github.com/poppinss/youch + const youch = new Youch(); + // Send response setResponseStatus(event, statusCode, statusMessage); setSecurityHeaders(event, true /* allow js */); @@ -70,3 +75,56 @@ export default defineNitroErrorHandler( ); } ); + +// ---- Source Map support ---- + +type SourceLoader = Parameters[0]; +type StackFrame = Parameters[0]; +async function sourceLoader(frame: StackFrame) { + if (!frame.fileName || frame.fileType !== "fs" || frame.type === "native") { + return; + } + + if (frame.type === "app") { + // prettier-ignore + const rawSourceMap = await readFile(`${frame.fileName}.map`, "utf8").catch(() => {}); + if (rawSourceMap) { + const consumer = await new SourceMapConsumer(rawSourceMap); + // prettier-ignore + const originalPosition = consumer.originalPositionFor({ line: frame.lineNumber!, column: frame.columnNumber! }); + if (originalPosition.source && originalPosition.line) { + // prettier-ignore + 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 } : undefined; +} + +async function loadStackTrace(error: any) { + const parsed = await new ErrorParser() + .defineSourceLoader(sourceLoader) + .parse(error); + + const stack = + error.message + + "\n" + + parsed.frames + .map((frame) => { + return frame.type === "app" || !frame.raw + ? `at ${frame.functionName || ""} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})` + : frame.raw; + }) + .join("\n"); + + Object.defineProperty(error, "stack", { value: stack }); + + if (error.cause) { + await loadStackTrace(error.cause); + } +} diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts index 647aed1940..783abb694d 100644 --- a/src/runtime/internal/error/prod.ts +++ b/src/runtime/internal/error/prod.ts @@ -1,5 +1,4 @@ import { - getRequestHeader, getRequestURL, getResponseHeader, send, From be71f9b4ee87bde0b37cf87891a44b6e3fc81b73 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 15 Jan 2025 00:12:42 +0100 Subject: [PATCH 12/16] small improvements --- src/runtime/internal/error/dev.ts | 48 +++++++++++++++++-------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index a9b0935a7e..c410fc483e 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -78,6 +78,28 @@ export default defineNitroErrorHandler( // ---- Source Map support ---- +async function loadStackTrace(error: any) { + 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"); + try { + Object.defineProperty(error, "stack", { value: stack }); + } catch { + /* ignore */ + } + if (error.cause) { + await loadStackTrace(error.cause); + } +} + type SourceLoader = Parameters[0]; type StackFrame = Parameters[0]; async function sourceLoader(frame: StackFrame) { @@ -102,29 +124,13 @@ async function sourceLoader(frame: StackFrame) { } const contents = await readFile(frame.fileName, "utf8").catch(() => {}); - return contents ? { contents } : undefined; } -async function loadStackTrace(error: any) { - const parsed = await new ErrorParser() - .defineSourceLoader(sourceLoader) - .parse(error); - - const stack = - error.message + - "\n" + - parsed.frames - .map((frame) => { - return frame.type === "app" || !frame.raw - ? `at ${frame.functionName || ""} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})` - : frame.raw; - }) - .join("\n"); - - Object.defineProperty(error, "stack", { value: stack }); - - if (error.cause) { - await loadStackTrace(error.cause); +function fmtFrame(frame: StackFrame) { + if (frame.type === "native") { + return frame.raw; } + const src = `${frame.fileName || ""}:${frame.lineNumber}:${frame.columnNumber})`; + return frame.functionName ? `at ${frame.functionName} (${src}` : `at ${src}`; } From e90a9295db78a70ee39351e8add9dcb4f61eba9c Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 15 Jan 2025 01:18:17 +0100 Subject: [PATCH 13/16] use for worker init errors as well --- src/core/dev-server/error.ts | 55 ------------------------------- src/core/dev-server/server.ts | 20 +++++------ src/runtime/internal/error/dev.ts | 2 +- 3 files changed, 10 insertions(+), 67 deletions(-) delete mode 100644 src/core/dev-server/error.ts diff --git a/src/core/dev-server/error.ts b/src/core/dev-server/error.ts deleted file mode 100644 index 9ec1ad71f4..0000000000 --- a/src/core/dev-server/error.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - type H3Event, - getResponseStatus, - getResponseStatusText, - send, - setResponseHeader, - setResponseStatus, -} from "h3"; -import type { NitroErrorHandler } from "nitropack/types"; - -function errorHandler(error: any, event: H3Event) { - setResponseHeader(event, "Content-Type", "text/html; charset=UTF-8"); - setResponseStatus(event, 503, "Server Unavailable"); - - let body; - let title; - if (error) { - title = `${getResponseStatus(event)} ${getResponseStatusText(event)}`; - body = `
${error.stack}
`; - } else { - title = "Reloading server..."; - body = - ""; - } - - return send( - event, - ` - - - - - ${error ? "" : ''} - ${title} - - - -
-
-
-

${title}

-
- ${body} -
- Check console logs for more information. -
-
-
- - -` - ); -} - -export default errorHandler as NitroErrorHandler; diff --git a/src/core/dev-server/server.ts b/src/core/dev-server/server.ts index 3e67162f96..e6f851b874 100644 --- a/src/core/dev-server/server.ts +++ b/src/core/dev-server/server.ts @@ -28,7 +28,10 @@ import { debounce } from "perfect-debounce"; import { servePlaceholder } from "serve-placeholder"; import serveStatic from "serve-static"; import { joinURL } from "ufo"; -import defaultErrorHandler from "./error"; +import consola from "consola"; +import defaultErrorHandler, { + loadStackTrace, +} from "../../runtime/internal/error/dev"; import { createVFSHandler } from "./vfs"; function initWorker(filename: string): Promise | undefined { @@ -49,14 +52,8 @@ function initWorker(filename: string): Promise | undefined { ) ); }); - worker.once("error", (error) => { - const newError = new Error(`[worker init] ${filename} failed`, { - cause: error, - }); - if (Error.captureStackTrace) { - Error.captureStackTrace(newError, initWorker); - } - reject(newError); + worker.once("error", async (error) => { + reject(error); }); const addressListener = (event: any) => { if (!event || !event?.address) { @@ -149,8 +146,9 @@ export function createDevServer(nitro: Nitro): NitroDevServer { .then(() => { lastError = undefined; }) - .catch((error) => { - console.error("[worker reload]", error); + .catch(async (error) => { + await loadStackTrace(error).catch(() => {}); + consola.error(error); lastError = error; }) .finally(() => { diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index c410fc483e..5530940156 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -78,7 +78,7 @@ export default defineNitroErrorHandler( // ---- Source Map support ---- -async function loadStackTrace(error: any) { +export async function loadStackTrace(error: any) { if (!(error instanceof Error)) { return; } From 3bebd090e006e4c70f7e60e29a2166fab41676c5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 15 Jan 2025 01:23:15 +0100 Subject: [PATCH 14/16] always call loadStackTrace with error handling --- src/runtime/internal/error/dev.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index 5530940156..6501d6e215 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -23,7 +23,7 @@ export default defineNitroErrorHandler( const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); // Load stack trace with source maps - await loadStackTrace(error); + await loadStackTrace(error).catch(() => {}); // Console output if (error.unhandled || error.fatal) { @@ -90,13 +90,11 @@ export async function loadStackTrace(error: any) { error.message + "\n" + parsed.frames.map((frame) => fmtFrame(frame)).join("\n"); - try { - Object.defineProperty(error, "stack", { value: stack }); - } catch { - /* ignore */ - } + + Object.defineProperty(error, "stack", { value: stack }); + if (error.cause) { - await loadStackTrace(error.cause); + await loadStackTrace(error.cause).catch(() => {}); } } From 9f50bdb3fe5990292d478643d4c3063a555bd946 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 15 Jan 2025 10:27:04 +0100 Subject: [PATCH 15/16] mask data for sensitive errors in prod --- src/runtime/internal/error/dev.ts | 7 ++++--- src/runtime/internal/error/prod.ts | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index 6501d6e215..33b515d8e6 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -17,16 +17,17 @@ import { defineNitroErrorHandler, setSecurityHeaders } from "./utils"; export default defineNitroErrorHandler( async function defaultNitroErrorHandler(error, event) { + const isSensitive = error.unhandled || error.fatal; const statusCode = error.statusCode || 500; const statusMessage = error.statusMessage || "Server Error"; // prettier-ignore const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); // Load stack trace with source maps - await loadStackTrace(error).catch(() => {}); + await loadStackTrace(error).catch(consola.error); // Console output - if (error.unhandled || error.fatal) { + if (isSensitive) { // prettier-ignore const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" ") consola.error( @@ -94,7 +95,7 @@ export async function loadStackTrace(error: any) { Object.defineProperty(error, "stack", { value: stack }); if (error.cause) { - await loadStackTrace(error.cause).catch(() => {}); + await loadStackTrace(error.cause).catch(consola.error); } } diff --git a/src/runtime/internal/error/prod.ts b/src/runtime/internal/error/prod.ts index 783abb694d..855fec2ed8 100644 --- a/src/runtime/internal/error/prod.ts +++ b/src/runtime/internal/error/prod.ts @@ -9,13 +9,14 @@ import { defineNitroErrorHandler, setSecurityHeaders } from "./utils"; export default defineNitroErrorHandler( function defaultNitroErrorHandler(error, event) { + const isSensitive = error.unhandled || error.fatal; const statusCode = error.statusCode || 500; const statusMessage = error.statusMessage || "Server Error"; // prettier-ignore const url = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).toString(); // Console output - if (error.unhandled || error.fatal) { + if (isSensitive) { // prettier-ignore const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" ") console.error( @@ -38,9 +39,8 @@ export default defineNitroErrorHandler( url, statusCode, statusMessage, - message: - error.unhandled || error.fatal ? "Server Error" : error.message, - data: error.data, + message: isSensitive ? "Server Error" : error.message, + data: isSensitive ? undefined : error.data, }, null, 2 From 28f4f3e6c97b370b1281090ce467aaa797f54563 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 15 Jan 2025 10:32:55 +0100 Subject: [PATCH 16/16] use youch ansi formatter :fire: --- src/runtime/internal/error/dev.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/runtime/internal/error/dev.ts b/src/runtime/internal/error/dev.ts index 33b515d8e6..0a6b3dbe52 100644 --- a/src/runtime/internal/error/dev.ts +++ b/src/runtime/internal/error/dev.ts @@ -26,19 +26,31 @@ export default defineNitroErrorHandler( // Load stack trace with source maps await loadStackTrace(error).catch(consola.error); + // https://github.com/poppinss/youch + const youch = new Youch(); + // Console output if (isSensitive) { // prettier-ignore const tags = [error.unhandled && "[unhandled]", error.fatal && "[fatal]"].filter(Boolean).join(" ") + + const columns = process.stderr.columns; + if (!columns) { + process.stdout.columns = 90; // Temporary workaround for youch wrapping issue + } + const ansiError = await ( + await youch.toANSI(error) + ).replaceAll(process.cwd(), "."); + if (!columns) { + process.stderr.columns = columns; + } + consola.error( `[nitro] [request error] ${tags} [${event.method}] ${url}\n\n`, - error + ansiError ); } - // https://github.com/poppinss/youch - const youch = new Youch(); - // Send response setResponseStatus(event, statusCode, statusMessage); setSecurityHeaders(event, true /* allow js */);