Skip to content

Commit 566776a

Browse files
authored
check if data is serializable (#5987)
* check data is serializable * ok we do need a separate pass after all, much simpler that way * update test * only test serializability in dev * Update packages/kit/src/runtime/server/index.js
1 parent a5f0506 commit 566776a

File tree

11 files changed

+84
-2
lines changed

11 files changed

+84
-2
lines changed

.changeset/shiny-scissors-listen.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+
Check that data is serializable

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export async function respond(request, options, state) {
267267
return {
268268
// TODO return `uses`, so we can reuse server data effectively
269269
data: await load_server_data({
270+
dev: options.dev,
270271
event,
271272
node,
272273
parent: async () => {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
161161
}
162162

163163
return await load_server_data({
164+
dev: options.dev,
164165
event,
165166
node,
166167
parent: async () => {

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { LoadURL, PrerenderingURL } from '../../../utils/url.js';
33
/**
44
* Calls the user's `load` function.
55
* @param {{
6+
* dev: boolean;
67
* event: import('types').RequestEvent;
78
* node: import('types').SSRNode | undefined;
89
* parent: () => Promise<Record<string, any>>;
910
* }} opts
1011
*/
11-
export async function load_server_data({ event, node, parent }) {
12+
export async function load_server_data({ dev, event, node, parent }) {
1213
if (!node?.server) return null;
1314

1415
const server_data = await node.server.load?.call(null, {
@@ -27,7 +28,13 @@ export async function load_server_data({ event, node, parent }) {
2728
url: event.url
2829
});
2930

30-
return server_data ? unwrap_promises(server_data) : null;
31+
const result = server_data ? await unwrap_promises(server_data) : null;
32+
33+
if (dev) {
34+
check_serializability(result, /** @type {string} */ (node.server_id), 'data');
35+
}
36+
37+
return result;
3138
}
3239

3340
/**
@@ -79,3 +86,42 @@ async function unwrap_promises(object) {
7986

8087
return unwrapped;
8188
}
89+
90+
/**
91+
* Check that the data can safely be serialized to JSON
92+
* @param {any} value
93+
* @param {string} id
94+
* @param {string} path
95+
*/
96+
function check_serializability(value, id, path) {
97+
const type = typeof value;
98+
99+
if (type === 'string' || type === 'boolean' || type === 'number' || type === 'undefined') {
100+
// primitives are fine
101+
return;
102+
}
103+
104+
if (type === 'object') {
105+
// nulls are fine...
106+
if (!value) return;
107+
108+
// ...so are plain arrays...
109+
if (Array.isArray(value)) {
110+
value.forEach((child, i) => {
111+
check_serializability(child, id, `${path}[${i}]`);
112+
});
113+
return;
114+
}
115+
116+
// ...and objects
117+
const tag = Object.prototype.toString.call(value);
118+
if (tag === '[object Object]') {
119+
for (const key in value) {
120+
check_serializability(value[key], id, `${path}.${key}`);
121+
}
122+
return;
123+
}
124+
}
125+
126+
throw new Error(`${path} returned from 'load' in ${id} cannot be serialized as JSON`);
127+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function respond_with_error({ event, options, state, status, error,
3535
const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
3636

3737
const server_data_promise = load_server_data({
38+
dev: options.dev,
3839
event,
3940
node: default_layout,
4041
parent: async () => ({})

packages/kit/src/vite/dev/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
119119
if (node.server) {
120120
const { module } = await resolve(node.server);
121121
result.server = module;
122+
result.server_id = node.server;
122123
}
123124

124125
// in dev we inline all styles to avoid FOUC. this gets populated lazily so that

packages/kit/test/apps/basics/src/routes/shadowed/+page.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<a href="/shadowed/no-get">no-get</a>
66
<a href="/shadowed/dynamic/foo">dynamic/foo</a>
77
<a href="/shadowed/missing-get">missing-get</a>
8+
<a href="/shadowed/serialization">serialization</a>
89

910
<form action="/shadowed/redirect-post" method="post">
1011
<button type="submit" id="redirect-post">redirect</button>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function load() {
2+
return {
3+
regex: /nope/
4+
};
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
/** @type {import('./$types').PageData} */
3+
export let data;
4+
</script>
5+
6+
<h1>{data.regex.test('nope')}</h1>

packages/kit/test/apps/basics/test/test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,18 @@ test.describe('Shadowed pages', () => {
277277
'Page data: {"sub":"sub","data":{"rootlayout":"rootlayout","layout":"layout"}}'
278278
);
279279
});
280+
281+
if (process.env.DEV) {
282+
test('Data must be serializable', async ({ page, clicknav }) => {
283+
await page.goto('/shadowed');
284+
await clicknav('[href="/shadowed/serialization"]');
285+
286+
expect(await page.textContent('h1')).toBe('500');
287+
expect(await page.textContent('#message')).toBe(
288+
'This is your custom error page saying: "data.regex returned from \'load\' in src/routes/shadowed/serialization/+page.server.js cannot be serialized as JSON"'
289+
);
290+
});
291+
}
280292
});
281293

282294
test.describe('Encoded paths', () => {

packages/kit/types/internal.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ export interface SSRNode {
221221
PUT?: Action;
222222
DELETE?: Action;
223223
};
224+
225+
// store this in dev so we can print serialization errors
226+
server_id?: string;
224227
}
225228

226229
export type SSRNodeLoader = () => Promise<SSRNode>;

0 commit comments

Comments
 (0)