From f555c519f298dca35ab226a8fbc3f3f94de71c9a Mon Sep 17 00:00:00 2001 From: Ondrej Ulehla Date: Wed, 22 Jul 2026 19:52:41 +0200 Subject: [PATCH] fix: don't invoke handleError for server-handled errors during preload When a preloaded route's server load throws error(), the branch loader's preload path returned before the server_data_nodes guard, so the client handleError hook ran with the raw server error node and status 500. Error-reporting integrations that filter 4xx by status (e.g. Sentry) therefore reported ordinary server-rendered 404s as client 500s whenever a link was hovered instead of clicked. Compute the error and status through the same guard as normal navigation before the preload return, so server-handled errors skip the client hook on the preload path too. Fixes #16456 --- .changeset/preload-server-error-handling.md | 5 ++++ packages/kit/src/runtime/client/client.js | 27 +++++++++---------- packages/kit/test/apps/basics/src/global.d.ts | 1 + .../kit/test/apps/basics/src/hooks.client.js | 4 +++ .../preloading/preload-error/+page.svelte | 16 +++++++++++ .../preload-error/target/+page.server.js | 5 ++++ .../preload-error/target/+page.svelte | 1 + .../kit/test/apps/basics/test/client.test.js | 11 ++++++++ 8 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 .changeset/preload-server-error-handling.md create mode 100644 packages/kit/test/apps/basics/src/routes/preloading/preload-error/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.svelte diff --git a/.changeset/preload-server-error-handling.md b/.changeset/preload-server-error-handling.md new file mode 100644 index 000000000000..e0688c76a2cd --- /dev/null +++ b/.changeset/preload-server-error-handling.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: do not invoke client `handleError` for errors already handled on the server when the navigation is a preload diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a0b858ba0ddb..e41f777a9b68 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1330,14 +1330,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) { }; } - if (preload && preload_tokens.has(preload)) { - return preload_error({ - error: await handle_error(err, { params, url, route: { id: route.id } }), - url, - params, - route - }); - } + const is_preload = !!preload && preload_tokens.has(preload); let status = get_status(err); /** @type {App.Error} */ @@ -1351,17 +1344,23 @@ async function load_route({ id, invalidating, url, params, route, preload }) { } else if (err instanceof HttpError) { error = err.body; } else { - // Referenced node could have been removed due to redeploy, check - const updated = await stores.updated.check(); - if (updated) { - // Before reloading, try to update the service worker if it exists - await update_service_worker(); - return await native_navigation(url); + if (!is_preload) { + // Referenced node could have been removed due to redeploy, check + const updated = await stores.updated.check(); + if (updated) { + // Before reloading, try to update the service worker if it exists + await update_service_worker(); + return await native_navigation(url); + } } error = await handle_error(err, { params, url, route: { id: route.id } }); } + if (is_preload) { + return preload_error({ error, url, params, route }); + } + const error_load = await load_nearest_error_page(i, branch, errors); if (error_load) { return get_navigation_result_from_branch({ diff --git a/packages/kit/test/apps/basics/src/global.d.ts b/packages/kit/test/apps/basics/src/global.d.ts index cb794387c759..9baf47fa31f1 100644 --- a/packages/kit/test/apps/basics/src/global.d.ts +++ b/packages/kit/test/apps/basics/src/global.d.ts @@ -5,6 +5,7 @@ declare global { pageContext: any; mounted: number; fulfil_navigation: (value: any) => void; + handle_error_calls: Array<{ status: number; message: string }>; promise: Promise; PUBLIC_DYNAMIC: string; } diff --git a/packages/kit/test/apps/basics/src/hooks.client.js b/packages/kit/test/apps/basics/src/hooks.client.js index 3749d91b5aa3..7358591f7f20 100644 --- a/packages/kit/test/apps/basics/src/hooks.client.js +++ b/packages/kit/test/apps/basics/src/hooks.client.js @@ -4,6 +4,10 @@ window.PUBLIC_DYNAMIC = env.PUBLIC_DYNAMIC; /** @type{import("@sveltejs/kit").HandleClientError} */ export function handleError({ error, event, status, message }) { + (window.handle_error_calls ??= []).push({ + status, + message: /** @type {Error} */ (error).message + }); return event.url.pathname.endsWith('404-fallback') ? undefined : { message: `${/** @type {Error} */ (error).message} (${status} ${message})` }; diff --git a/packages/kit/test/apps/basics/src/routes/preloading/preload-error/+page.svelte b/packages/kit/test/apps/basics/src/routes/preloading/preload-error/+page.svelte new file mode 100644 index 000000000000..778e7c93b224 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/preloading/preload-error/+page.svelte @@ -0,0 +1,16 @@ + + + + +{#if result} +
{JSON.stringify(result)}
+{/if} diff --git a/packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.server.js b/packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.server.js new file mode 100644 index 000000000000..34be726e305c --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.server.js @@ -0,0 +1,5 @@ +import { error } from '@sveltejs/kit'; + +export function load() { + error(404, 'Not found'); +} diff --git a/packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.svelte b/packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.svelte new file mode 100644 index 000000000000..d341a8b9250a --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/preloading/preload-error/target/+page.svelte @@ -0,0 +1 @@ +

this page never renders successfully

diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 8b5d6df581ee..a0e7faa669ca 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1789,6 +1789,17 @@ test.describe('Shallow routing', () => { }); }); +test.describe('Preloading', () => { + test('does not invoke handleError for server load errors during preload', async ({ page }) => { + await page.goto('/preloading/preload-error'); + await page.locator('button').click(); + await expect(page.locator('#result')).toContainText('"type":"loaded"'); + + // the server already handled the error; the client hook must not run + expect(await page.evaluate(() => window.handle_error_calls)).toBeUndefined(); + }); +}); + test.describe('reroute', () => { test('Apply reroute during client side navigation', async ({ page, clicknav }) => { await page.goto('/reroute/basic');