Skip to content

Commit 4d9cc7f

Browse files
authored
fix: layout load data not serialized on error page (#14395)
Fixes #14298 When a load function errors, we still need the serialized data of the successfully run layout load functions.
1 parent e5ac4a3 commit 4d9cc7f

File tree

10 files changed

+98
-45
lines changed

10 files changed

+98
-45
lines changed

.changeset/legal-peas-agree.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: layout load data not serialized on error page

packages/kit/src/runtime/server/page/data_serializer.js

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,61 +18,72 @@ import {
1818
*/
1919
export function server_data_serializer(event, event_state, options) {
2020
let promise_id = 1;
21+
let max_nodes = -1;
2122

2223
const iterator = create_async_iterator();
2324
const global = get_global_name(options);
2425

25-
/** @param {any} thing */
26-
function replacer(thing) {
27-
if (typeof thing?.then === 'function') {
28-
const id = promise_id++;
29-
30-
const promise = thing
31-
.then(/** @param {any} data */ (data) => ({ data }))
32-
.catch(
33-
/** @param {any} error */ async (error) => ({
34-
error: await handle_error_and_jsonify(event, event_state, options, error)
35-
})
36-
)
37-
.then(
38-
/**
39-
* @param {{data: any; error: any}} result
40-
*/
41-
async ({ data, error }) => {
42-
let str;
43-
try {
44-
str = devalue.uneval(error ? [, error] : [data], replacer);
45-
} catch {
46-
error = await handle_error_and_jsonify(
47-
event,
48-
event_state,
49-
options,
50-
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
51-
);
52-
data = undefined;
53-
str = devalue.uneval([, error], replacer);
26+
/** @param {number} index */
27+
function get_replacer(index) {
28+
/** @param {any} thing */
29+
return function replacer(thing) {
30+
if (typeof thing?.then === 'function') {
31+
const id = promise_id++;
32+
33+
const promise = thing
34+
.then(/** @param {any} data */ (data) => ({ data }))
35+
.catch(
36+
/** @param {any} error */ async (error) => ({
37+
error: await handle_error_and_jsonify(event, event_state, options, error)
38+
})
39+
)
40+
.then(
41+
/**
42+
* @param {{data: any; error: any}} result
43+
*/
44+
async ({ data, error }) => {
45+
let str;
46+
try {
47+
str = devalue.uneval(error ? [, error] : [data], replacer);
48+
} catch {
49+
error = await handle_error_and_jsonify(
50+
event,
51+
event_state,
52+
options,
53+
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
54+
);
55+
data = undefined;
56+
str = devalue.uneval([, error], replacer);
57+
}
58+
59+
return {
60+
index,
61+
str: `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`
62+
};
5463
}
64+
);
5565

56-
return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`;
57-
}
58-
);
66+
iterator.add(promise);
5967

60-
iterator.add(promise);
61-
62-
return `${global}.defer(${id})`;
63-
} else {
64-
for (const key in options.hooks.transport) {
65-
const encoded = options.hooks.transport[key].encode(thing);
66-
if (encoded) {
67-
return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`;
68+
return `${global}.defer(${id})`;
69+
} else {
70+
for (const key in options.hooks.transport) {
71+
const encoded = options.hooks.transport[key].encode(thing);
72+
if (encoded) {
73+
return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`;
74+
}
6875
}
6976
}
70-
}
77+
};
7178
}
7279

7380
const strings = /** @type {string[]} */ ([]);
7481

7582
return {
83+
set_max_nodes(i) {
84+
max_nodes = i;
85+
},
86+
7687
add_node(i, node) {
7788
try {
7889
if (!node) {
@@ -84,7 +95,7 @@ export function server_data_serializer(event, event_state, options) {
8495
const payload = { type: 'data', data: node.data, uses: serialize_uses(node) };
8596
if (node.slash) payload.slash = node.slash;
8697

87-
strings[i] = devalue.uneval(payload, replacer);
98+
strings[i] = devalue.uneval(payload, get_replacer(i));
8899
} catch (e) {
89100
// @ts-expect-error
90101
e.path = e.path.slice(1);
@@ -97,8 +108,17 @@ export function server_data_serializer(event, event_state, options) {
97108
const close = `</script>\n`;
98109

99110
return {
100-
data: `[${compact(strings).join(',')}]`,
101-
chunks: promise_id > 1 ? iterator.iterate((str) => open + str + close) : null
111+
data: `[${compact(max_nodes > -1 ? strings.slice(0, max_nodes) : strings).join(',')}]`,
112+
chunks:
113+
promise_id > 1
114+
? iterator.iterate(({ index, str }) => {
115+
if (max_nodes > -1 && index >= max_nodes) {
116+
return '';
117+
}
118+
119+
return open + str + close;
120+
})
121+
: null
102122
};
103123
}
104124
};

packages/kit/src/runtime/server/page/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ export async function render_page(
281281
let j = i;
282282
while (!branch[j]) j -= 1;
283283

284+
data_serializer.set_max_nodes(j + 1);
285+
284286
const layouts = compact(branch.slice(0, j + 1));
285287
const nodes = new PageNodes(layouts.map((layout) => layout.node));
286288

@@ -303,7 +305,7 @@ export async function render_page(
303305
server_data: null
304306
}),
305307
fetched,
306-
data_serializer: server_data_serializer(event, event_state, options)
308+
data_serializer
307309
});
308310
}
309311
}

packages/kit/src/runtime/server/page/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface Cookie {
4545
export type ServerDataSerializer = {
4646
add_node(i: number, node: ServerDataNode | null): void;
4747
get_data(csp: Csp): { data: string; chunks: AsyncIterable<string> | null };
48+
set_max_nodes(i: number): void;
4849
};
4950

5051
export type ServerDataSerializerJson = {

packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+error.svelte

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('./$types').LayoutServerLoad} */
2+
export function load() {
3+
return {
4+
answer: 42
5+
};
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
/** @type {import('./$types').LayoutData} */
3+
export let data;
4+
</script>
5+
6+
<slot />
7+
8+
<div id="error-layout-data">{data.answer}</div>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { error } from '@sveltejs/kit';
2+
3+
/** @type {import('@sveltejs/kit').Load} */
4+
export async function load() {
5+
error(404, 'Not found');
6+
}

packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+page.svelte

Whitespace-only changes.

packages/kit/test/apps/basics/test/cross-platform/test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,11 @@ test.describe('Errors', () => {
331331
expect(/** @type {Response} */ (response).status()).toBe(555);
332332
});
333333

334+
test('server-side error from load() still has layout data', async ({ page }) => {
335+
await page.goto('/errors/load-error-server/layout-data');
336+
expect(await page.textContent('#error-layout-data')).toBe('42');
337+
});
338+
334339
test('error in endpoint', async ({ page, read_errors }) => {
335340
const res = await page.goto('/errors/endpoint');
336341

0 commit comments

Comments
 (0)