From f878bdb68f2ce0d003f0f1cc9091ae7efb9b509a Mon Sep 17 00:00:00 2001 From: hamboomger Date: Tue, 15 Jul 2025 11:08:55 +0100 Subject: [PATCH 1/5] Sanitize server stack trace on errors --- packages/kit/src/runtime/server/index.js | 11 ++++- packages/kit/src/runtime/server/utils.js | 49 +++++++++++++++++++ playgrounds/basic/src/routes/+page.svelte | 1 + .../src/routes/force-error/+page.server.ts | 5 ++ .../basic/src/routes/force-error/+page.svelte | 4 ++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 playgrounds/basic/src/routes/force-error/+page.server.ts create mode 100644 playgrounds/basic/src/routes/force-error/+page.svelte diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 738da671f8ca..ab38e5cfec3a 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -3,6 +3,7 @@ import { set_private_env, set_public_env, set_safe_public_env } from '../shared- import { options, get_hooks } from '__SERVER__/internal.js'; import { DEV } from 'esm-env'; import { filter_private_env, filter_public_env } from '../../utils/env.js'; +import { format_server_error } from './utils.js'; import { prerendering } from '__sveltekit/environment'; import { set_read_implementation, set_manifest } from '__sveltekit/server'; import { set_app } from './app.js'; @@ -77,8 +78,14 @@ export class Server { handle: module.handle || (({ event, resolve }) => resolve(event)), handleError: module.handleError || - (({ status, error }) => - console.error((status === 404 && /** @type {Error} */ (error)?.message) || error)), + (({ status, error, event }) => { + const error_message = format_server_error( + status, + /** @type {Error} */ (error), + event + ); + console.error(error_message); + }), handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)), reroute: module.reroute || (() => {}), transport: module.transport || {} diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 57420e7404ce..0c758e4c5c86 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -180,3 +180,52 @@ export function has_prerendered_path(manifest, pathname) { (pathname.at(-1) === '/' && manifest._.prerendered_routes.has(pathname.slice(0, -1))) ); } + +/** + * Formats the error into a nice message with sanitized stack trace + * @param {number} status + * @param {Error} error + * @param {import('@sveltejs/kit').RequestEvent} event + */ +export function format_server_error(status, error, event) { + const formatted_text = `\x1b[1;31m[${status}] ${event.request.method} ${event.url.pathname}\x1b[0m\n`; + + if (status === 404) { + return formatted_text + error.message; + } + + return formatted_text + clean_up_stack_trace(error); +} + +/** + * Provides a refined stack trace by excluding lines following the last occurrence of a line containing +page. +layout. or +server. + * @param {Error} error + */ +export function clean_up_stack_trace(error) { + const stack_trace = error.stack?.split('\n') ?? []; + const last_line_from_src_code = find_last_index( + stack_trace, + (line) => + ['+page.', '+layout.', '+server.'].find((snippet) => line.includes(snippet)) !== undefined + ); + + if (last_line_from_src_code === -1) { + // default to the whole stack trace + return error.stack; + } + + return stack_trace.slice(0, last_line_from_src_code + 1).join('\n'); +} + +/** + * @param {any[]} array + * @param {(item: any, index: number, array: any[]) => boolean} predicate + */ +export function find_last_index(array, predicate) { + for (let i = array.length - 1; i >= 0; i--) { + if (predicate(array[i], i, array)) { + return i; + } + } + return -1; +} diff --git a/playgrounds/basic/src/routes/+page.svelte b/playgrounds/basic/src/routes/+page.svelte index b3872f2e7f2c..a1c819624339 100644 --- a/playgrounds/basic/src/routes/+page.svelte +++ b/playgrounds/basic/src/routes/+page.svelte @@ -12,4 +12,5 @@ diff --git a/playgrounds/basic/src/routes/force-error/+page.server.ts b/playgrounds/basic/src/routes/force-error/+page.server.ts new file mode 100644 index 000000000000..5de264103568 --- /dev/null +++ b/playgrounds/basic/src/routes/force-error/+page.server.ts @@ -0,0 +1,5 @@ +export const actions = { + default: async () => { + throw new Error('test'); + } +}; diff --git a/playgrounds/basic/src/routes/force-error/+page.svelte b/playgrounds/basic/src/routes/force-error/+page.svelte new file mode 100644 index 000000000000..96467abec651 --- /dev/null +++ b/playgrounds/basic/src/routes/force-error/+page.svelte @@ -0,0 +1,4 @@ +
+
+ +
From 68b3fc427c76837f30236f90a46d3c409b18ce82 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 20 Aug 2025 11:23:18 -0400 Subject: [PATCH 2/5] use findLastIndex --- packages/kit/src/runtime/server/utils.js | 21 +++------------------ packages/kit/tsconfig.json | 2 +- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 02a78579079f..eda84b17df35 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -202,17 +202,15 @@ export function format_server_error(status, error, event) { return formatted_text + clean_up_stack_trace(error); } +const route_file_regex = /\+(page|layout|server)/; + /** * Provides a refined stack trace by excluding lines following the last occurrence of a line containing +page. +layout. or +server. * @param {Error} error */ export function clean_up_stack_trace(error) { const stack_trace = error.stack?.split('\n') ?? []; - const last_line_from_src_code = find_last_index( - stack_trace, - (line) => - ['+page.', '+layout.', '+server.'].find((snippet) => line.includes(snippet)) !== undefined - ); + const last_line_from_src_code = stack_trace.findLastIndex((line) => route_file_regex.test(line)); if (last_line_from_src_code === -1) { // default to the whole stack trace @@ -222,19 +220,6 @@ export function clean_up_stack_trace(error) { return stack_trace.slice(0, last_line_from_src_code + 1).join('\n'); } -/** - * @param {any[]} array - * @param {(item: any, index: number, array: any[]) => boolean} predicate - */ -export function find_last_index(array, predicate) { - for (let i = array.length - 1; i >= 0; i--) { - if (predicate(array[i], i, array)) { - return i; - } - } - return -1; -} - /** * Returns the filename without the extension. e.g., `+page.server`, `+page`, etc. * @param {string | undefined} node_id diff --git a/packages/kit/tsconfig.json b/packages/kit/tsconfig.json index 8b927b041456..f87ec5cfe3cb 100644 --- a/packages/kit/tsconfig.json +++ b/packages/kit/tsconfig.json @@ -4,7 +4,7 @@ "checkJs": true, "noEmit": true, "strict": true, - "target": "es2022", + "target": "es2023", "module": "node16", "moduleResolution": "node16", "allowSyntheticDefaultImports": true, From 86727dd5cc03a9e9c87e65f8e10753050ffc3f7b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 20 Aug 2025 11:42:28 -0400 Subject: [PATCH 3/5] add preceding newline --- packages/kit/src/runtime/server/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index eda84b17df35..d729875fae3a 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -193,7 +193,7 @@ export function has_prerendered_path(manifest, pathname) { * @param {import('@sveltejs/kit').RequestEvent} event */ export function format_server_error(status, error, event) { - const formatted_text = `\x1b[1;31m[${status}] ${event.request.method} ${event.url.pathname}\x1b[0m\n`; + const formatted_text = `\n\x1b[1;31m[${status}] ${event.request.method} ${event.url.pathname}\x1b[0m\n`; if (status === 404) { return formatted_text + error.message; From 8b225ce5dd5ab63a1332e6b57f26cee74be779c8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 20 Aug 2025 11:59:41 -0400 Subject: [PATCH 4/5] only clean up stack trace in dev, since in prod it's a no-op. make paths project-relative, and use as the boundary --- packages/kit/src/runtime/server/utils.js | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index d729875fae3a..91dbd1cb8f99 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -199,18 +199,37 @@ export function format_server_error(status, error, event) { return formatted_text + error.message; } - return formatted_text + clean_up_stack_trace(error); + return formatted_text + (DEV ? clean_up_stack_trace(error) : error.stack); } -const route_file_regex = /\+(page|layout|server)/; +/** + * In dev, tidy up stack traces by making paths relative to the current project directory + * @param {string} file + */ +let relative = (file) => file; + +if (DEV) { + try { + const path = await import('node:path'); + const process = await import('node:process'); + + relative = (file) => path.relative(process.cwd(), file); + } catch { + // do nothing + } +} /** * Provides a refined stack trace by excluding lines following the last occurrence of a line containing +page. +layout. or +server. * @param {Error} error */ export function clean_up_stack_trace(error) { - const stack_trace = error.stack?.split('\n') ?? []; - const last_line_from_src_code = stack_trace.findLastIndex((line) => route_file_regex.test(line)); + const stack_trace = (error.stack?.split('\n') ?? []).map((line) => { + return line.replace(/\((.+)(:\d+:\d+)\)$/, (_, file, loc) => `(${relative(file)}${loc})`); + }); + + // progressive enhancement for people who haven't configured kit.files.src to something else + const last_line_from_src_code = stack_trace.findLastIndex((line) => /\(src[\\/]/.test(line)); if (last_line_from_src_code === -1) { // default to the whole stack trace From dfb570c961b95421982fbb82c5e6420ee2192ada Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 20 Aug 2025 12:10:51 -0400 Subject: [PATCH 5/5] changeset --- .changeset/lucky-sites-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lucky-sites-wash.md diff --git a/.changeset/lucky-sites-wash.md b/.changeset/lucky-sites-wash.md new file mode 100644 index 000000000000..fc4a2f5fb6bb --- /dev/null +++ b/.changeset/lucky-sites-wash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: better server-side error logging