Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/legal-peas-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: layout load data not serialized on error page
108 changes: 64 additions & 44 deletions packages/kit/src/runtime/server/page/data_serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,61 +18,72 @@ import {
*/
export function server_data_serializer(event, event_state, options) {
let promise_id = 1;
let max_nodes = -1;

const iterator = create_async_iterator();
const global = get_global_name(options);

/** @param {any} thing */
function replacer(thing) {
if (typeof thing?.then === 'function') {
const id = promise_id++;

const promise = thing
.then(/** @param {any} data */ (data) => ({ data }))
.catch(
/** @param {any} error */ async (error) => ({
error: await handle_error_and_jsonify(event, event_state, options, error)
})
)
.then(
/**
* @param {{data: any; error: any}} result
*/
async ({ data, error }) => {
let str;
try {
str = devalue.uneval(error ? [, error] : [data], replacer);
} catch {
error = await handle_error_and_jsonify(
event,
event_state,
options,
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
);
data = undefined;
str = devalue.uneval([, error], replacer);
/** @param {number} index */
function get_replacer(index) {
/** @param {any} thing */
return function replacer(thing) {
if (typeof thing?.then === 'function') {
const id = promise_id++;

const promise = thing
.then(/** @param {any} data */ (data) => ({ data }))
.catch(
/** @param {any} error */ async (error) => ({
error: await handle_error_and_jsonify(event, event_state, options, error)
})
)
.then(
/**
* @param {{data: any; error: any}} result
*/
async ({ data, error }) => {
let str;
try {
str = devalue.uneval(error ? [, error] : [data], replacer);
} catch {
error = await handle_error_and_jsonify(
event,
event_state,
options,
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
);
data = undefined;
str = devalue.uneval([, error], replacer);
}

return {
index,
str: `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`
};
}
);

return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`;
}
);
iterator.add(promise);

iterator.add(promise);

return `${global}.defer(${id})`;
} else {
for (const key in options.hooks.transport) {
const encoded = options.hooks.transport[key].encode(thing);
if (encoded) {
return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`;
return `${global}.defer(${id})`;
} else {
for (const key in options.hooks.transport) {
const encoded = options.hooks.transport[key].encode(thing);
if (encoded) {
return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`;
}
}
}
}
};
}

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

return {
set_max_nodes(i) {
max_nodes = i;
},

add_node(i, node) {
try {
if (!node) {
Expand All @@ -84,7 +95,7 @@ export function server_data_serializer(event, event_state, options) {
const payload = { type: 'data', data: node.data, uses: serialize_uses(node) };
if (node.slash) payload.slash = node.slash;

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

return {
data: `[${compact(strings).join(',')}]`,
chunks: promise_id > 1 ? iterator.iterate((str) => open + str + close) : null
data: `[${compact(max_nodes > -1 ? strings.slice(0, max_nodes) : strings).join(',')}]`,
chunks:
promise_id > 1
? iterator.iterate(({ index, str }) => {
if (max_nodes > -1 && index >= max_nodes) {
return '';
}

return open + str + close;
})
: null
};
}
};
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ export async function render_page(
let j = i;
while (!branch[j]) j -= 1;

data_serializer.set_max_nodes(j + 1);

const layouts = compact(branch.slice(0, j + 1));
const nodes = new PageNodes(layouts.map((layout) => layout.node));

Expand All @@ -303,7 +305,7 @@ export async function render_page(
server_data: null
}),
fetched,
data_serializer: server_data_serializer(event, event_state, options)
data_serializer
});
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/page/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface Cookie {
export type ServerDataSerializer = {
add_node(i: number, node: ServerDataNode | null): void;
get_data(csp: Csp): { data: string; chunks: AsyncIterable<string> | null };
set_max_nodes(i: number): void;
};

export type ServerDataSerializerJson = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('./$types').LayoutServerLoad} */
export function load() {
return {
answer: 42
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
/** @type {import('./$types').LayoutData} */
export let data;
</script>

<slot />

<div id="error-layout-data">{data.answer}</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { error } from '@sveltejs/kit';

/** @type {import('@sveltejs/kit').Load} */
export async function load() {
error(404, 'Not found');
}
5 changes: 5 additions & 0 deletions packages/kit/test/apps/basics/test/cross-platform/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,11 @@ test.describe('Errors', () => {
expect(/** @type {Response} */ (response).status()).toBe(555);
});

test('server-side error from load() still has layout data', async ({ page }) => {
await page.goto('/errors/load-error-server/layout-data');
expect(await page.textContent('#error-layout-data')).toBe('42');
});

test('error in endpoint', async ({ page, read_errors }) => {
const res = await page.goto('/errors/endpoint');

Expand Down
Loading