diff --git a/.changeset/clever-boxes-feel.md b/.changeset/clever-boxes-feel.md new file mode 100644 index 000000000000..c33ab43d5931 --- /dev/null +++ b/.changeset/clever-boxes-feel.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: serialize server `load` data before passing to universal `load`, to handle mutations diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index d70276550469..dbe4e7c2ef17 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -1,9 +1,10 @@ import { DEV } from 'esm-env'; +import * as devalue from 'devalue'; import { disable_search, make_trackable } from '../../../utils/url.js'; import { validate_depends, validate_load_response } from '../../shared.js'; import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; import { record_span } from '../../telemetry/record_span.js'; -import { get_node_type } from '../utils.js'; +import { clarify_devalue_error, get_node_type } from '../utils.js'; import { base64_encode, text_decoder } from '../../utils.js'; /** @@ -232,11 +233,38 @@ export async function load_data({ }, fn: async (current) => { const traced_event = merge_tracing(event, current); - return await with_request_store({ event: traced_event, state: event_state }, () => - load.call(null, { + return await with_request_store({ event: traced_event, state: event_state }, () => { + /** @type {Record | null} */ + let data = null; + + return load.call(null, { url: event.url, params: event.params, - data: server_data_node?.data ?? null, + get data() { + if (data === null && server_data_node?.data != null) { + /** @type {Record any>} */ + const reducers = {}; + + /** @type {Record any>} */ + const revivers = {}; + + for (const key in event_state.transport) { + reducers[key] = event_state.transport[key].encode; + revivers[key] = event_state.transport[key].decode; + } + + // run it through devalue so that the developer can't accidentally mutate it + try { + data = devalue.parse(devalue.stringify(server_data_node.data, reducers), revivers); + } catch (e) { + // @ts-expect-error + e.path = e.path.slice(1); + throw new Error(clarify_devalue_error(event, /** @type {any} */ (e))); + } + } + + return data; + }, route: event.route, fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts), setHeaders: event.setHeaders, @@ -244,8 +272,8 @@ export async function load_data({ parent, untrack: (fn) => fn(), tracing: traced_event.tracing - }) - ); + }); + }); } }); diff --git a/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js b/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js index 72f7c4d96c37..0cf6e8feaa3d 100644 --- a/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js +++ b/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js @@ -5,5 +5,11 @@ export async function load({ fetch, data, url }) { const res = await fetch(new URL('/load/serialization/fetched-from-shared.json', url.origin)); const { b } = await res.json(); - return { a, b, c: a + b }; + // check that this doesn't mutate the original object + // and make the server data unserializable + // @ts-expect-error + data.sum = () => a + b; + + // @ts-expect-error + return { a, b, c: data.sum() }; }