diff --git a/.changeset/modern-dodos-hang.md b/.changeset/modern-dodos-hang.md new file mode 100644 index 000000000000..20d094ff853f --- /dev/null +++ b/.changeset/modern-dodos-hang.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +feat: add error result type to `preloadData` diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c4f78548655c..20f688afb34b 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -989,10 +989,14 @@ function diff_search_params(old_url, new_url) { } /** - * @param {Omit & { error: App.Error }} opts + * @param {Omit & { error: App.Error; status: number }} opts * @returns {import('./types.js').NavigationFinished} */ -function preload_error({ error, url, route, params }) { +function preload_error({ error, status, url, route, params }) { + // we skipped loading the error page, so we need to use the current page + // store, but we still pass the updated status to the preloadData function + const new_page = clone_page(page); + new_page.status = status; return { type: 'loaded', state: { @@ -1003,7 +1007,7 @@ function preload_error({ error, url, route, params }) { branch: [] }, props: { - page: clone_page(page), + page: new_page, constructors: [] } }; @@ -1068,12 +1072,14 @@ async function load_route({ id, invalidating, url, params, route, preload }) { } catch (error) { const handled_error = await handle_error(error, { url, params, route: { id } }); + const status = get_status(error); + if (preload_tokens.has(preload)) { - return preload_error({ error: handled_error, url, params, route }); + return preload_error({ error: handled_error, status, url, params, route }); } return load_root_error_page({ - status: get_status(error), + status, error: handled_error, url, route @@ -1154,20 +1160,23 @@ async function load_route({ id, invalidating, url, params, route, preload }) { if (err instanceof Redirect) { return { type: 'redirect', + status: err.status, location: err.location }; } + let status = get_status(err); + if (preload_tokens.has(preload)) { return preload_error({ error: await handle_error(err, { params, url, route: { id: route.id } }), + status, url, params, route }); } - let status = get_status(err); /** @type {App.Error} */ let error; @@ -2185,7 +2194,7 @@ export function refreshAll({ includeLoadFunctions = true } = {}) { * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete. * * @param {string} href Page to preload - * @returns {Promise<{ type: 'loaded'; status: number; data: Record } | { type: 'redirect'; location: string }>} + * @returns {Promise<({ type: 'loaded'; data: Record } | { type: 'redirect'; location: string } | { type: 'error'; error: App.Error }) & { status: number; }>} */ export async function preloadData(href) { if (!BROWSER) { @@ -2203,11 +2212,21 @@ export async function preloadData(href) { if (result.type === 'redirect') { return { type: result.type, + status: result.status, location: result.location }; } const { status, data } = result.props.page ?? page; + + if (result.type === 'loaded' && result.state.error) { + return { + type: 'error', + status, + error: result.state.error + }; + } + return { type: result.type, status, data }; } diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 927b5b7a1041..627f3429fc38 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -77,6 +77,7 @@ export type NavigationResult = NavigationRedirect | NavigationFinished; export type NavigationRedirect = { type: 'redirect'; + status: number; location: string; }; diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 83e16c3a9d12..ae4e54e419d9 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -182,6 +182,7 @@ export function redirect_json_response(redirect) { return json_response( /** @type {import('types').ServerRedirectNode} */ ({ type: 'redirect', + status: redirect.status, location: redirect.location }) ); diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 15d315905ab6..4272b273d69b 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -258,6 +258,7 @@ export async function render_page( if (state.prerendering && should_prerender_data) { const body = JSON.stringify({ type: 'redirect', + status: err.status, location: err.location }); diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index ae12ae644c3f..5d27042a6bfe 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -283,6 +283,7 @@ export interface RouteData { export type ServerRedirectNode = { type: 'redirect'; + status: number; location: string; }; diff --git a/packages/kit/test/apps/basics/src/routes/routing/preloading/error/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/preloading/error/+page.svelte new file mode 100644 index 000000000000..e5772e7ec5d0 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/preloading/error/+page.svelte @@ -0,0 +1,21 @@ + + + + + + +

{data.type} {data.status} {data.error?.message}

diff --git a/packages/kit/test/apps/basics/src/routes/routing/preloading/error/[id]/+page.js b/packages/kit/test/apps/basics/src/routes/routing/preloading/error/[id]/+page.js new file mode 100644 index 000000000000..bedc745a9574 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/preloading/error/[id]/+page.js @@ -0,0 +1,11 @@ +import { error } from '@sveltejs/kit'; + +export function load({ params }) { + if (params.id === '404') { + error(404, { message: 'Not found' }); + } + + if (params.id === '500') { + throw new Error('Oopsie'); + } +} diff --git a/packages/kit/test/apps/basics/src/routes/routing/preloading/error/[id]/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/preloading/error/[id]/+page.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index a2be35a47dc6..291d557f2da0 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -766,6 +766,18 @@ test.describe('Prefetching', () => { await app.goto('/routing/preloading/preload-error'); await expect(page.locator('p')).toHaveText('hello'); }); + + test('preloads errors', async ({ page }) => { + await page.goto('/routing/preloading/error'); + + await expect(page.locator('p')).toHaveText('undefined undefined undefined'); + + page.locator('button', { hasText: '404' }).click(); + await expect(page.locator('p')).toHaveText('error 404 Not found'); + + page.locator('button', { hasText: '500' }).click(); + await expect(page.locator('p')).toHaveText('error 500 Oopsie (500 Internal Error)'); + }); }); test.describe('Routing', () => { diff --git a/packages/kit/test/prerendering/basics/test/tests.spec.js b/packages/kit/test/prerendering/basics/test/tests.spec.js index e3972a1d369d..50fd7123ba1c 100644 --- a/packages/kit/test/prerendering/basics/test/tests.spec.js +++ b/packages/kit/test/prerendering/basics/test/tests.spec.js @@ -48,6 +48,7 @@ test('renders a server-side redirect', () => { expect(data).toEqual({ type: 'redirect', + status: 301, location: 'https://example.com/redirected' }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index c41dd5ea556e..e58d4c3738bf 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3056,13 +3056,17 @@ declare module '$app/navigation' { * * @param href Page to preload * */ - export function preloadData(href: string): Promise<{ + export function preloadData(href: string): Promise<({ type: "loaded"; - status: number; data: Record; } | { type: "redirect"; location: string; + } | { + type: "error"; + error: App.Error; + }) & { + status: number; }>; /** * Programmatically imports the code for routes that haven't yet been fetched.