From a039c7da43d7c26f38c9a3e9b3a1549e65299f39 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 5 Sep 2022 17:09:39 +0200 Subject: [PATCH 01/26] handlError returns App.PageData shape --- packages/kit/src/exports/index.js | 21 ++--- .../src/exports/vite/build/build_server.js | 6 +- packages/kit/src/exports/vite/dev/index.js | 39 ++++----- packages/kit/src/runtime/client/client.js | 49 ++++++----- packages/kit/src/runtime/client/start.js | 2 +- packages/kit/src/runtime/client/types.d.ts | 6 +- packages/kit/src/runtime/control.js | 22 +++-- packages/kit/src/runtime/server/data/index.js | 20 ++--- packages/kit/src/runtime/server/index.js | 4 +- packages/kit/src/runtime/server/page/index.js | 35 +++----- .../kit/src/runtime/server/page/render.js | 17 +--- .../runtime/server/page/respond_with_error.js | 18 ++-- .../kit/src/runtime/server/page/types.d.ts | 6 -- packages/kit/src/runtime/server/utils.js | 80 +++++------------- packages/kit/src/runtime/server/utils.spec.js | 45 ---------- packages/kit/test/apps/basics/src/hooks.js | 35 +++++++- .../test/apps/basics/src/routes/+error.svelte | 2 - .../routes/errors/page-endpoint/+error.svelte | 6 +- .../kit/test/apps/basics/test/server.test.js | 11 +-- packages/kit/test/apps/basics/test/test.js | 82 +++++++------------ packages/kit/test/utils.d.ts | 5 +- packages/kit/types/ambient.d.ts | 7 ++ packages/kit/types/index.d.ts | 16 ++-- packages/kit/types/internal.d.ts | 11 +-- 24 files changed, 206 insertions(+), 339 deletions(-) delete mode 100644 packages/kit/src/runtime/server/utils.spec.js diff --git a/packages/kit/src/exports/index.js b/packages/kit/src/exports/index.js index 0a924350779a..ac7d2cb288c3 100644 --- a/packages/kit/src/exports/index.js +++ b/packages/kit/src/exports/index.js @@ -1,22 +1,17 @@ import { HttpError, Redirect } from '../runtime/control.js'; +// For some reason we need to type the params as well here, +// JSdoc doesn't seem to like @type with function overloads /** - * Creates an `HttpError` object with an HTTP status code and an optional message. - * This object, if thrown during request handling, will cause SvelteKit to - * return an error response without invoking `handleError` + * @type {import('@sveltejs/kit').error} * @param {number} status - * @param {string | undefined} [message] + * @param {any} message */ export function error(status, message) { return new HttpError(status, message); } -/** - * Creates a `Redirect` object. If thrown during request handling, SvelteKit will - * return a redirect response. - * @param {number} status - * @param {string} location - */ +/** @type {import('@sveltejs/kit').redirect} */ export function redirect(status, location) { if (isNaN(status) || status < 300 || status > 399) { throw new Error('Invalid status code'); @@ -25,11 +20,7 @@ export function redirect(status, location) { return new Redirect(status, location); } -/** - * Generates a JSON `Response` object from the supplied data. - * @param {any} data - * @param {ResponseInit} [init] - */ +/** @type {import('@sveltejs/kit').json} */ export function json(data, init) { // TODO deprecate this in favour of `Response.json` when it's // more widely supported diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index 137d358eeb75..1fb239363023 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -58,9 +58,8 @@ export class Server { check_origin: ${s(config.kit.csrf.checkOrigin)}, }, dev: false, - get_stack: error => String(error), // for security handle_error: (error, event) => { - this.options.hooks.handleError({ + return this.options.hooks.handleError({ error, event, @@ -69,8 +68,7 @@ export class Server { get request() { throw new Error('request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'); } - }); - error.stack = this.options.get_stack(error); + }) ?? { message: 'Internal Error' }; }, hooks: null, manifest, diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 4ce5b8fb9b27..df8b3dcf642e 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -381,28 +381,29 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) { check_origin: svelte_config.kit.csrf.checkOrigin }, dev: true, - get_stack: (error) => fix_stack_trace(error), handle_error: (error, event) => { - hooks.handleError({ - error: new Proxy(error, { - get: (target, property) => { - if (property === 'stack') { - return fix_stack_trace(error); + return ( + hooks.handleError({ + error: new Proxy(error, { + get: (target, property) => { + if (property === 'stack') { + return fix_stack_trace(error); + } + + return Reflect.get(target, property, target); } - - return Reflect.get(target, property, target); + }), + event, + + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error( + 'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details' + ); } - }), - event, - - // TODO remove for 1.0 - // @ts-expect-error - get request() { - throw new Error( - 'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details' - ); - } - }); + }) ?? { message: 'Internal Error' } + ); }, hooks, manifest, diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index f866a04c1b0e..3421df75b44f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -4,7 +4,6 @@ import { make_trackable, decode_params, normalize_path } from '../../utils/url.j import { find_anchor, get_base_uri, scroll_state } from './utils.js'; import { lock_fetch, unlock_fetch, initial_fetch, subsequent_fetch } from './fetcher.js'; import { parse } from './parse.js'; -import { error } from '../../exports/index.js'; import Root from '__GENERATED__/root.svelte'; import { nodes, server_loads, dictionary, matchers } from '__GENERATED__/client-manifest.js'; @@ -420,7 +419,7 @@ export function create_client({ target, base, trailing_slash }) { * params: Record; * branch: Array; * status: number; - * error: HttpError | Error | null; + * error: App.PageError | null; * route: import('types').CSRRoute | null; * validation_errors?: Record | null; * }} opts @@ -787,12 +786,8 @@ export function create_client({ target, base, trailing_slash }) { parent_changed = true; if (server_data_node?.type === 'error') { - if (server_data_node.httperror) { - // reconstruct as an HttpError - throw error(server_data_node.httperror.status, server_data_node.httperror.message); - } else { - throw server_data_node.error; - } + // rethrow and catch below + throw server_data_node; } return load_node({ @@ -821,17 +816,29 @@ export function create_client({ target, base, trailing_slash }) { if (loaders[i]) { try { branch.push(await branch_promises[i]); - } catch (e) { - const error = normalize_error(e); - - if (error instanceof Redirect) { + } catch (err) { + if (err instanceof Redirect) { return { type: 'redirect', - location: error.location + location: err.location }; } - const status = e instanceof HttpError ? e.status : 500; + let status = 500; + /** @type {App.PageError} */ + let error; + + if (/** @type {any} */ (err)?.type === 'error') { + // this is the server error rethrown above, reconstruct but don't invoke + // the client error handler; it should've already been handled on the server + status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status; + error = /** @type {import('types').ServerErrorNode} */ (err).error; + } else if (err instanceof HttpError) { + status = err.status; + error = err.body; + } else { + error = { message: 'TODO client hook' }; + } while (i--) { if (errors[i]) { @@ -949,7 +956,7 @@ export function create_client({ target, base, trailing_slash }) { params, branch: [root_layout, root_error], status, - error, + error, // TODO invoke client hook route: null }); } @@ -1356,7 +1363,7 @@ export function create_client({ target, base, trailing_slash }) { _hydrate: async ({ status, - error: original_error, // TODO get rid of this + error, node_ids, params, routeId, @@ -1393,15 +1400,7 @@ export function create_client({ target, base, trailing_slash }) { params, branch: await Promise.all(branch_promises), status, - error: /** @type {import('../server/page/types').SerializedHttpError} */ (original_error) - ?.__is_http_error - ? new HttpError( - /** @type {import('../server/page/types').SerializedHttpError} */ ( - original_error - ).status, - original_error.message - ) - : original_error, + error, validation_errors, route: routes.find((route) => route.id === routeId) ?? null }); diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js index 7e8dd591a91a..384dcf2c8972 100644 --- a/packages/kit/src/runtime/client/start.js +++ b/packages/kit/src/runtime/client/start.js @@ -8,7 +8,7 @@ import { set_public_env } from '../env-public.js'; * env: Record; * hydrate: { * status: number; - * error: Error | (import('../server/page/types').SerializedHttpError); + * error: App.PageError; * node_ids: number[]; * params: Record; * routeId: string | null; diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index ad59df44a2fb..4e581f754431 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -8,8 +8,6 @@ import { prefetchRoutes } from '$app/navigation'; import { CSRPageNode, CSRPageNodeLoader, CSRRoute, Uses } from 'types'; -import { HttpError } from '../control.js'; -import { SerializedHttpError } from '../server/page/types.js'; export interface Client { // public API, exposed via $app/navigation @@ -25,7 +23,7 @@ export interface Client { // private API _hydrate: (opts: { status: number; - error: Error | SerializedHttpError; + error: App.PageError; node_ids: number[]; params: Record; routeId: string | null; @@ -83,7 +81,7 @@ export interface DataNode { export interface NavigationState { branch: Array; - error: HttpError | Error | null; + error: App.PageError | null; params: Record; route: CSRRoute | null; session_id: number; diff --git a/packages/kit/src/runtime/control.js b/packages/kit/src/runtime/control.js index 912572472dff..d00547601b8e 100644 --- a/packages/kit/src/runtime/control.js +++ b/packages/kit/src/runtime/control.js @@ -1,23 +1,21 @@ export class HttpError { - // without these, things like `$page.error.stack` will error. we don't want to - // include a stack for these sorts of errors, but we also don't want red - // squigglies everywhere, so this feels like a not-terribile compromise - name = 'HttpError'; - - /** @type {void} */ - stack = undefined; - /** * @param {number} status - * @param {string | undefined} message + * @param {{message: string} extends App.PageError ? (App.PageError | string | undefined) : App.PageError} body */ - constructor(status, message) { + constructor(status, body) { this.status = status; - this.message = message ?? `Error: ${status}`; + if (typeof body === 'string') { + this.body = { message: body }; + } else if (body) { + this.body = body; + } else { + this.body = { message: `Error: ${status}` }; + } } toString() { - return this.message; + return JSON.stringify(this.body); } } diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 25296344fca5..3f2c3654a5ba 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -2,7 +2,7 @@ import { HttpError, Redirect } from '../../control.js'; import { normalize_error } from '../../../utils/error.js'; import { once } from '../../../utils/functions.js'; import { load_server_data } from '../page/load_data.js'; -import { data_response, error_to_pojo } from '../utils.js'; +import { data_response, handle_error_and_jsonify } from '../utils.js'; import { normalize_path } from '../../../utils/url.js'; import { DATA_SUFFIX } from '../../../constants.js'; @@ -93,9 +93,7 @@ export async function render_data(event, route, options, state) { let length = promises.length; const nodes = await Promise.all( promises.map((p, i) => - p.catch((e) => { - const error = normalize_error(e); - + p.catch((error) => { if (error instanceof Redirect) { throw error; } @@ -103,18 +101,10 @@ export async function render_data(event, route, options, state) { // Math.min because array isn't guaranteed to resolve in order length = Math.min(length, i + 1); - if (error instanceof HttpError) { - return /** @type {import('types').ServerErrorNode} */ ({ - type: 'error', - httperror: { ...error } - }); - } - - options.handle_error(error, event); - return /** @type {import('types').ServerErrorNode} */ ({ type: 'error', - error: error_to_pojo(error, options.get_stack) + error: handle_error_and_jsonify(event, options, error), + status: error instanceof HttpError ? error.status : undefined }); }) ) @@ -140,7 +130,7 @@ export async function render_data(event, route, options, state) { return data_response(server_data); } else { // TODO make it clearer that this was an unexpected error - return data_response(error_to_pojo(error, options.get_stack)); + return data_response(handle_error_and_jsonify(event, options, error)); } } } diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index ec142c6ece25..f8344d87dcea 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -8,6 +8,7 @@ import { decode_params, disable_search, normalize_path } from '../../utils/url.j import { exec } from '../../utils/routing.js'; import { render_data } from './data/index.js'; import { DATA_SUFFIX } from '../../constants.js'; +import { HttpError } from '../control.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ @@ -332,7 +333,8 @@ export async function respond(request, options, state) { // so we need to make an actual HTTP request return await fetch(request); } catch (e) { - const error = coalesce_to_error(e); + // HttpError can come from endpoint - TODO should it be handled there instead? + const error = e instanceof HttpError ? e : coalesce_to_error(e); return handle_fatal_error(event, options, error); } } diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index c704cdc83d15..83acdeaf2bff 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -4,10 +4,10 @@ import { render_response } from './render.js'; import { respond_with_error } from './respond_with_error.js'; import { method_not_allowed, - error_to_pojo, allowed_methods, get_option, - static_error_page + static_error_page, + handle_error_and_jsonify } from '../utils.js'; import { create_fetch } from './fetch.js'; import { HttpError, Redirect } from '../../control.js'; @@ -235,13 +235,13 @@ export async function render_page(event, route, page, options, state, resolve_op branch.push({ node, server_data, data }); } catch (e) { - const error = normalize_error(e); + const err = normalize_error(e); - if (error instanceof Redirect) { + if (err instanceof Redirect) { if (state.prerendering && should_prerender_data) { const body = `window.__sveltekit_data = ${JSON.stringify({ type: 'redirect', - location: error.location + location: err.location })}`; state.prerendering.dependencies.set(data_pathname, { @@ -250,14 +250,11 @@ export async function render_page(event, route, page, options, state, resolve_op }); } - return redirect_response(error.status, error.location); + return redirect_response(err.status, err.location); } - if (!(error instanceof HttpError)) { - options.handle_error(/** @type {Error} */ (error), event); - } - - const status = error instanceof HttpError ? error.status : 500; + const status = err instanceof HttpError ? err.status : 500; + const error = handle_error_and_jsonify(event, options, err); while (i--) { if (page.errors[i]) { @@ -289,11 +286,7 @@ export async function render_page(event, route, page, options, state, resolve_op // if we're still here, it means the error happened in the root layout, // which means we have to fall back to error.html - return static_error_page( - options, - status, - /** @type {HttpError | Error} */ (error).message - ); + return static_error_page(options, status, error.message); } } else { // push an empty slot so we can rewind past gaps to the @@ -335,14 +328,12 @@ export async function render_page(event, route, page, options, state, resolve_op } catch (error) { // if we end up here, it means the data loaded successfull // but the page failed to render, or that a prerendering error occurred - options.handle_error(/** @type {Error} */ (error), event); - return await respond_with_error({ event, options, state, status: 500, - error: /** @type {Error} */ (error), + error: handle_error_and_jsonify(event, options, error), resolve_opts }); } @@ -382,11 +373,7 @@ export async function handle_json_request(event, options, mod) { return redirect_response(error.status, error.location); } - if (!(error instanceof HttpError)) { - options.handle_error(error, event); - } - - return json(error_to_pojo(error, options.get_stack), { + return json(handle_error_and_jsonify(event, options, error), { status: error instanceof HttpError ? error.status : 500 }); } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index ec03e07122dd..522bc7a9a8b9 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -5,8 +5,6 @@ import { hash } from '../../hash.js'; import { serialize_data } from './serialize_data.js'; import { s } from '../../../utils/misc.js'; import { Csp } from './csp.js'; -import { serialize_error } from '../utils.js'; -import { HttpError } from '../../control.js'; // TODO rename this function/module @@ -25,7 +23,7 @@ const updated = { * state: import('types').SSRState; * page_config: { ssr: boolean; csr: boolean }; * status: number; - * error: HttpError | Error | null; + * error: App.PageError | null; * event: import('types').RequestEvent; * resolve_opts: import('types').RequiredResolveOptions; * validation_errors: Record | undefined; @@ -68,12 +66,6 @@ export async function render_response({ let rendered; - const stack = error instanceof HttpError ? undefined : error?.stack; - - if (error && options.dev && !(error instanceof HttpError)) { - error.stack = options.get_stack(error); - } - if (page_config.ssr) { /** @type {Record} */ const props = { @@ -207,7 +199,7 @@ export async function render_response({ env: ${s(options.public_env)}, hydrate: ${page_config.ssr ? `{ status: ${status}, - error: ${error && serialize_error(error, e => e.stack)}, + error: ${error}, node_ids: [${branch.map(({ node }) => node.index).join(', ')}], params: ${devalue(event.params)}, routeId: ${s(event.routeId)}, @@ -346,11 +338,6 @@ export async function render_response({ } } - if (error && options.dev && !(error instanceof HttpError)) { - // reset stack, otherwise it may be 'fixed' a second time - error.stack = stack; - } - return new Response(html, { status, headers diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index 739bc06b3674..cc48a4382bc9 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -1,7 +1,11 @@ import { render_response } from './render.js'; import { load_data, load_server_data } from './load_data.js'; -import { coalesce_to_error } from '../../../utils/error.js'; -import { GENERIC_ERROR, get_option, static_error_page } from '../utils.js'; +import { + handle_error_and_jsonify, + GENERIC_ERROR, + get_option, + static_error_page +} from '../utils.js'; import { create_fetch } from './fetch.js'; /** @@ -16,7 +20,7 @@ import { create_fetch } from './fetch.js'; * options: SSROptions; * state: SSRState; * status: number; - * error: Error; + * error: App.PageError; * resolve_opts: import('types').RequiredResolveOptions; * }} opts */ @@ -82,11 +86,7 @@ export async function respond_with_error({ event, options, state, status, error, resolve_opts, validation_errors: undefined }); - } catch (err) { - const error = coalesce_to_error(err); - - options.handle_error(error, event); - - return static_error_page(options, 500, error.message); + } catch (error) { + return static_error_page(options, 500, handle_error_and_jsonify(event, options, error).message); } } diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index 4da2b3d18171..4a9d3395ee90 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -37,9 +37,3 @@ export interface CspOpts { dev: boolean; prerender: boolean; } - -export interface SerializedHttpError extends Pick { - name: 'HttpError'; - stack: ''; - __is_http_error: true; -} diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 437de80e53c9..6b8cb66c4d1f 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -21,57 +21,6 @@ export function is_pojo(body) { return true; } -/** - * Serialize an error into a JSON string through `error_to_pojo`. - * This is necessary because `JSON.stringify(error) === '{}'` - * - * @param {Error | HttpError} error - * @param {(error: Error) => string | undefined} get_stack - */ -export function serialize_error(error, get_stack) { - return JSON.stringify(error_to_pojo(error, get_stack)); -} - -/** - * Transform an error into a POJO, by copying its `name`, `message` - * and (in dev) `stack`, plus any custom properties, plus recursively - * serialized `cause` properties. - * Our own HttpError gets a meta property attached so we can identify it on the client. - * - * @param {HttpError | Error } error - * @param {(error: Error) => string | undefined} get_stack - */ -export function error_to_pojo(error, get_stack) { - if (error instanceof HttpError) { - return /** @type {import('./page/types').SerializedHttpError} */ ({ - message: error.message, - status: error.status, - __is_http_error: true // TODO we should probably make this unnecessary - }); - } - - const { - name, - message, - stack, - // @ts-expect-error i guess typescript doesn't know about error.cause yet - cause, - ...custom - } = error; - - /** @type {Record} */ - const object = { name, message, stack: get_stack(error) }; - - if (cause) object.cause = error_to_pojo(cause, get_stack); - - for (const key in custom) { - // @ts-expect-error - object[key] = custom[key]; - } - - return object; -} - // TODO: Remove for 1.0 /** @param {Record} mod */ export function check_method_names(mod) { @@ -181,16 +130,11 @@ export function static_error_page(options, status, message) { /** * @param {import('types').RequestEvent} event * @param {import('types').SSROptions} options - * @param {Error} error + * @param {Error | HttpError} error */ export function handle_fatal_error(event, options, error) { - let status = 500; - - if (error instanceof HttpError) { - status = error.status; - } else { - options.handle_error(error, event); - } + const status = error instanceof HttpError ? error.status : 500; + const body = handle_error_and_jsonify(event, options, error); // ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it const type = negotiate(event.request.headers.get('accept') || 'text/html', [ @@ -199,11 +143,25 @@ export function handle_fatal_error(event, options, error) { ]); if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') { - return new Response(serialize_error(error, options.get_stack), { + return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json; charset=utf-8' } }); } - return static_error_page(options, status, error.message); + return static_error_page(options, status, body.message); +} + +/** + * @param {import('types').RequestEvent} event + * @param {import('types').SSROptions} options + * @param {any} error + * @returns {App.PageError} + */ +export function handle_error_and_jsonify(event, options, error) { + if (error instanceof HttpError) { + return error.body; + } else { + return options.handle_error(error, event); + } } diff --git a/packages/kit/src/runtime/server/utils.spec.js b/packages/kit/src/runtime/server/utils.spec.js deleted file mode 100644 index 8ec55eefe025..000000000000 --- a/packages/kit/src/runtime/server/utils.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import { test } from 'uvu'; -import * as assert from 'uvu/assert'; -import { serialize_error } from './utils.js'; - -test('serialize_error', () => { - class FancyError extends Error { - name = 'FancyError'; - fancy = true; - - /** - * @param {string} message - * @param {{ - * cause?: Error - * }} [options] - */ - constructor(message, options) { - // @ts-expect-error go home typescript ur drunk - super(message, options); - } - } - - const error = new FancyError('something went wrong', { - cause: new Error('sorry') - }); - - const serialized = serialize_error(error, (error) => error.stack); - - assert.equal( - serialized, - JSON.stringify({ - name: 'FancyError', - message: 'something went wrong', - stack: error.stack, - cause: { - name: 'Error', - message: 'sorry', - // @ts-expect-error - stack: error.cause.stack - }, - fancy: true - }) - ); -}); - -test.run(); diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js index 6a40df117da3..cb8e9807cff3 100644 --- a/packages/kit/test/apps/basics/src/hooks.js +++ b/packages/kit/test/apps/basics/src/hooks.js @@ -1,6 +1,38 @@ import fs from 'fs'; import cookie from 'cookie'; import { sequence } from '@sveltejs/kit/hooks'; +import { HttpError } from '../../../../src/runtime/control'; + +/** + * Transform an error into a POJO, by copying its `name`, `message` + * and (in dev) `stack`, plus any custom properties, plus recursively + * serialized `cause` properties. + * + * @param {HttpError | Error } error + * @param {(error: Error) => string | undefined} get_stack + */ +export function error_to_pojo(error, get_stack) { + if (error instanceof HttpError) { + return { + status: error.status, + ...error.body + }; + } + + const { name, message, stack, cause, ...custom } = error; + + /** @type {Record} */ + const object = { name, message, stack: get_stack(error) }; + + // @ts-ignore + if (cause) object.cause = error_to_pojo(cause, get_stack); + + for (const key in custom) { + object[key] = custom[key]; + } + + return object; +} /** @type {import('@sveltejs/kit').HandleError} */ export const handleError = ({ event, error }) => { @@ -11,8 +43,9 @@ export const handleError = ({ event, error }) => { const errors = fs.existsSync('test/errors.json') ? JSON.parse(fs.readFileSync('test/errors.json', 'utf8')) : {}; - errors[event.url.pathname] = error.stack || error.message; + errors[event.url.pathname] = error_to_pojo(error, (error) => error.stack); fs.writeFileSync('test/errors.json', JSON.stringify(errors)); + return { message: error.message }; }; export const handle = sequence( diff --git a/packages/kit/test/apps/basics/src/routes/+error.svelte b/packages/kit/test/apps/basics/src/routes/+error.svelte index d1897f37b61c..476d730cd604 100644 --- a/packages/kit/test/apps/basics/src/routes/+error.svelte +++ b/packages/kit/test/apps/basics/src/routes/+error.svelte @@ -10,8 +10,6 @@

This is your custom error page saying: "{$page.error.message}"

-
{$page.error.stack}
-