-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat: allow error boundaries to catch errors on the server #15308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
f9077f6
4dc9665
59867d4
ed183f7
4747589
081cc30
54c76be
4201313
893d207
61cfd7a
2ad2d98
46901d8
8fcff53
54d51d5
51d4ac0
2f1e4d3
21ee2cb
2c15482
fe4aab5
d738ed7
6de75b6
4546cb3
0b46132
bb79369
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@sveltejs/kit': minor | ||
| --- | ||
|
|
||
| feat: allow error boundaries to catch errors on the server |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -589,7 +589,18 @@ async function initialize(result, target, hydrate) { | |
| props: { ...result.props, stores, components }, | ||
| hydrate, | ||
| // @ts-ignore Svelte 5 specific: asynchronously instantiate the component, i.e. don't call flushSync | ||
| sync: false | ||
| sync: false, | ||
| // @ts-ignore Svelte 5 specific: transformError allows to transform errors before they are passed to boundaries | ||
| transformError: __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__ | ||
| ? /** @param {unknown} error */ (error) => | ||
| app.hooks.handleError({ | ||
| error, | ||
| // @ts-expect-error TODO - what do we pass here? Nothing, and the types are adjusted accordingly in SvelteKit 3? | ||
dummdidumm marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| event: null, | ||
| status: get_status(error), | ||
| message: get_message(error) | ||
| }) | ||
| : undefined | ||
| }); | ||
|
|
||
| // Wait for a microtask in case svelte experimental async is enabled, | ||
|
|
@@ -625,13 +636,23 @@ async function initialize(result, target, hydrate) { | |
| * url: URL; | ||
| * params: Record<string, string>; | ||
| * branch: Array<import('./types.js').BranchNode | undefined>; | ||
| * errors?: Array<import('types').CSRPageNodeLoader | undefined>; | ||
| * status: number; | ||
| * error: App.Error | null; | ||
| * route: import('types').CSRRoute | null; | ||
| * form?: Record<string, any> | null; | ||
| * }} opts | ||
| */ | ||
| function get_navigation_result_from_branch({ url, params, branch, status, error, route, form }) { | ||
| async function get_navigation_result_from_branch({ | ||
| url, | ||
| params, | ||
| branch, | ||
| errors, | ||
| status, | ||
| error, | ||
| route, | ||
| form | ||
| }) { | ||
| /** @type {import('types').TrailingSlash} */ | ||
| let slash = 'never'; | ||
|
|
||
|
|
@@ -666,6 +687,30 @@ function get_navigation_result_from_branch({ url, params, branch, status, error, | |
| } | ||
| }; | ||
|
|
||
| if (errors && __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__) { | ||
| let last_idx = -1; | ||
| result.props.errors = ( | ||
|
||
| await Promise.all( | ||
| branch.map((b, i) => { | ||
| if (!b) return null; | ||
|
|
||
| // Find the closest error component up to the previous branch | ||
| while (i > last_idx && !errors[i]) i -= 1; | ||
| last_idx = i; | ||
| return errors[i]?.() | ||
| .then((e) => e.component) | ||
| .catch(() => undefined); | ||
| }) | ||
| ) | ||
| ) | ||
| // filter out indexes where there was no branch, but keep indexes where there was a branch but no error component | ||
| .filter((e) => e !== null); | ||
|
|
||
| if (error) { | ||
| result.props.error = error; | ||
| } | ||
| } | ||
|
|
||
| if (form !== undefined) { | ||
| result.props.form = form; | ||
| } | ||
|
|
@@ -1197,6 +1242,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) { | |
| url, | ||
| params, | ||
| branch: branch.slice(0, error_load.idx).concat(error_load.node), | ||
| errors, | ||
| status, | ||
| error, | ||
| route | ||
|
|
@@ -1216,6 +1262,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) { | |
| url, | ||
| params, | ||
| branch, | ||
| errors, | ||
| status: 200, | ||
| error: null, | ||
| route, | ||
|
|
@@ -1321,6 +1368,7 @@ async function load_root_error_page({ status, error, url, route }) { | |
| branch: [root_layout, root_error], | ||
| status, | ||
| error, | ||
| errors: [], | ||
| route: null | ||
| }); | ||
| } catch (error) { | ||
|
|
@@ -2401,12 +2449,13 @@ export async function set_nearest_error_page(error, status = 500) { | |
|
|
||
| const error_load = await load_nearest_error_page(current.branch.length, branch, route.errors); | ||
| if (error_load) { | ||
| const navigation_result = get_navigation_result_from_branch({ | ||
| const navigation_result = await get_navigation_result_from_branch({ | ||
| url, | ||
| params: current.params, | ||
| branch: branch.slice(0, error_load.idx).concat(error_load.node), | ||
| status, | ||
| error, | ||
| // do not set errors, we haven't changed the page so the previous ones are still current | ||
| route | ||
| }); | ||
|
|
||
|
|
@@ -2829,12 +2878,13 @@ async function _hydrate( | |
| } | ||
| } | ||
|
|
||
| result = get_navigation_result_from_branch({ | ||
| result = await get_navigation_result_from_branch({ | ||
| url, | ||
| params, | ||
| branch, | ||
| status, | ||
| error, | ||
| errors: parsed_route?.errors, // TODO load earlier? | ||
| form, | ||
| route: parsed_route ?? null | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -299,6 +299,7 @@ export async function render_page( | |
| }, | ||
| status, | ||
| error, | ||
| // TODO error components | ||
| branch: layouts.concat({ | ||
| node, | ||
| data: null, | ||
|
|
@@ -337,6 +338,32 @@ export async function render_page( | |
| }); | ||
| } | ||
|
|
||
| /** @type {Array<import('types').SSRComponent | undefined> | undefined} */ | ||
| let error_components; | ||
| if (options.server_error_boundaries && ssr) { | ||
|
||
| let last_idx = -1; | ||
| error_components = ( | ||
| await Promise.all( | ||
| branch.map((b, i) => { | ||
| if (!b) return null; | ||
|
|
||
| // Find the closest error component up to the previous branch | ||
| while (i > last_idx && page.errors[i] === undefined) i -= 1; | ||
| last_idx = i; | ||
|
|
||
| const idx = page.errors[i]; | ||
| if (idx == null) return undefined; | ||
|
|
||
| return manifest._.nodes[idx]?.() | ||
| .then((e) => e.component?.()) | ||
| .catch(() => undefined); | ||
| }) | ||
| ) | ||
| ) | ||
| // filter out indexes where there was no branch, but keep indexes where there was a branch but no error component | ||
| .filter((e) => e !== null); | ||
| } | ||
|
|
||
| return await render_response({ | ||
| event, | ||
| event_state, | ||
|
|
@@ -350,11 +377,11 @@ export async function render_page( | |
| }, | ||
| status, | ||
| error: null, | ||
| branch: ssr === false ? [] : compact(branch), | ||
| branch: !ssr ? [] : compact(branch), | ||
| action_result, | ||
| fetched, | ||
| data_serializer: | ||
| ssr === false ? server_data_serializer(event, event_state, options) : data_serializer | ||
| data_serializer: !ssr ? server_data_serializer(event, event_state, options) : data_serializer, | ||
| error_components | ||
| }); | ||
| } catch (e) { | ||
| // a remote function could have thrown a redirect during render | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.