diff --git a/.changeset/young-apricots-dance.md b/.changeset/young-apricots-dance.md
new file mode 100644
index 000000000000..8e6c722f9d2b
--- /dev/null
+++ b/.changeset/young-apricots-dance.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Account for POST bodies when serializing fetches
diff --git a/packages/kit/src/runtime/client/renderer.js b/packages/kit/src/runtime/client/renderer.js
index b1165012f2c3..047671e201d1 100644
--- a/packages/kit/src/runtime/client/renderer.js
+++ b/packages/kit/src/runtime/client/renderer.js
@@ -1,5 +1,6 @@
import { writable } from 'svelte/store';
-import { normalize } from '../load';
+import { hash } from '../hash.js';
+import { normalize } from '../load.js';
/** @param {any} value */
function page_store(value) {
@@ -37,7 +38,14 @@ function page_store(value) {
*/
function initial_fetch(resource, opts) {
const url = typeof resource === 'string' ? resource : resource.url;
- const script = document.querySelector(`script[type="svelte-data"][url="${url}"]`);
+
+ let selector = `script[type="svelte-data"][url="${url}"]`;
+
+ if (opts && typeof opts.body === 'string') {
+ selector += `[body="${hash(opts.body)}"]`;
+ }
+
+ const script = document.querySelector(selector);
if (script) {
const { body, ...init } = JSON.parse(script.textContent);
return Promise.resolve(new Response(body, init));
diff --git a/packages/kit/src/runtime/hash.js b/packages/kit/src/runtime/hash.js
new file mode 100644
index 000000000000..d88fa0a56f50
--- /dev/null
+++ b/packages/kit/src/runtime/hash.js
@@ -0,0 +1,13 @@
+/** @param {string | Uint8Array} value */
+export function hash(value) {
+ let hash = 5381;
+ let i = value.length;
+
+ if (typeof value === 'string') {
+ while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
+ } else {
+ while (i) hash = (hash * 33) ^ value[--i];
+ }
+
+ return (hash >>> 0).toString(36);
+}
diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js
index a8b94646dd06..0c728e6df4e5 100644
--- a/packages/kit/src/runtime/server/page/load_node.js
+++ b/packages/kit/src/runtime/server/page/load_node.js
@@ -42,6 +42,7 @@ export async function load_node({
/** @type {Array<{
* url: string;
+ * body: string;
* json: string;
* }>} */
const fetched = [];
@@ -145,16 +146,21 @@ export async function load_node({
}
}
+ if (opts.body && typeof opts.body !== 'string') {
+ // per https://developer.mozilla.org/en-US/docs/Web/API/Request/Request, this can be a
+ // Blob, BufferSource, FormData, URLSearchParams, USVString, or ReadableStream object.
+ // non-string bodies are irksome to deal with, but luckily aren't particularly useful
+ // in this context anyway, so we take the easy route and ban them
+ throw new Error('Request body must be a string');
+ }
+
const rendered = await respond(
{
host: request.host,
method: opts.method || 'GET',
headers,
path: resolved,
- // TODO per https://developer.mozilla.org/en-US/docs/Web/API/Request/Request, this can be a
- // Blob, BufferSource, FormData, URLSearchParams, USVString, or ReadableStream object
- // @ts-ignore
- rawBody: opts.body,
+ rawBody: /** @type {string} */ (opts.body),
query: new URLSearchParams(search)
},
options,
@@ -189,11 +195,14 @@ export async function load_node({
if (key !== 'etag' && key !== 'set-cookie') headers[key] = value;
}
- // prettier-ignore
- fetched.push({
- url,
- json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape(body)}}`
- });
+ if (!opts.body || typeof opts.body === 'string') {
+ // prettier-ignore
+ fetched.push({
+ url,
+ body: /** @type {string} */ (opts.body),
+ json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape(body)}}`
+ });
+ }
return body;
}
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 04f2217620d1..f7ffc96b5d8c 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -1,5 +1,6 @@
import devalue from 'devalue';
import { writable } from 'svelte/store';
+import { hash } from '../../hash.js';
const s = JSON.stringify;
@@ -29,7 +30,7 @@ export async function render_response({
const js = new Set(options.entry.js);
const styles = new Set();
- /** @type {Array<{ url: string, json: string }>} */
+ /** @type {Array<{ url: string, body: string, json: string }>} */
const serialized_data = [];
let rendered;
@@ -156,7 +157,11 @@ export async function render_response({
: `${rendered.html}
${serialized_data
- .map(({ url, json }) => ``)
+ .map(({ url, body, json }) => {
+ return body
+ ? ``
+ : ``;
+ })
.join('\n\n\t\t\t')}
`.replace(/^\t{2}/gm, '');
diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts
index 7c4a653ab977..cc7cf9dfa801 100644
--- a/packages/kit/src/runtime/server/page/types.d.ts
+++ b/packages/kit/src/runtime/server/page/types.d.ts
@@ -4,6 +4,6 @@ export type Loaded = {
node: SSRNode;
loaded: NormalizedLoadOutput;
context: Record;
- fetched: Array<{ url: string; json: string }>;
+ fetched: Array<{ url: string; body: string; json: string }>;
uses_credentials: boolean;
};
diff --git a/packages/kit/test/apps/basics/src/routes/load/_tests.js b/packages/kit/test/apps/basics/src/routes/load/_tests.js
index f1e321e9e209..6c60df9382c4 100644
--- a/packages/kit/test/apps/basics/src/routes/load/_tests.js
+++ b/packages/kit/test/apps/basics/src/routes/load/_tests.js
@@ -8,7 +8,7 @@ export default function (test, is_dev) {
assert.equal(await page.textContent('h1'), 'bar == bar?');
});
- test('data is serialized', null, async ({ base, page, capture_requests, js }) => {
+ test('GET fetches are serialized', null, async ({ base, page, capture_requests, js }) => {
const requests = await capture_requests(async () => {
await page.goto(`${base}/load/serialization`);
});
@@ -29,6 +29,40 @@ export default function (test, is_dev) {
);
});
+ test('POST fetches are serialized', null, async ({ base, page, capture_requests, js }) => {
+ const requests = await capture_requests(async () => {
+ await page.goto(`${base}/load/serialization-post`);
+ });
+
+ assert.equal(await page.textContent('h1'), 'a: X');
+ assert.equal(await page.textContent('h2'), 'b: Y');
+
+ const payload_a =
+ '{"status":200,"statusText":"","headers":{"content-type":"text/plain;charset=UTF-8"},"body":"X"}';
+
+ const payload_b =
+ '{"status":200,"statusText":"","headers":{"content-type":"text/plain;charset=UTF-8"},"body":"Y"}';
+
+ if (!js) {
+ // by the time JS has run, hydration will have nuked these scripts
+ const script_contents_a = await page.innerHTML(
+ 'script[type="svelte-data"][url="/load/serialization-post.json"][body="3t25"]'
+ );
+
+ const script_contents_b = await page.innerHTML(
+ 'script[type="svelte-data"][url="/load/serialization-post.json"][body="3t24"]'
+ );
+
+ assert.equal(script_contents_a, payload_a, 'Page should contain serialized data');
+ assert.equal(script_contents_b, payload_b, 'Page should contain serialized data');
+ }
+
+ assert.ok(
+ !requests.some((r) => r.endsWith('/load/serialization.json')),
+ 'Should not load JSON over the wire'
+ );
+ });
+
test('json string is returned', '/load/relay', async ({ page }) => {
assert.equal(await page.textContent('h1'), '42');
});
diff --git a/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js b/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js
new file mode 100644
index 000000000000..5ea0ae3ff398
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js
@@ -0,0 +1,6 @@
+/** @type {import('@sveltejs/kit').RequestHandler} */
+export function post(request) {
+ return {
+ body: request.body.toUpperCase()
+ };
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/serialization-post.svelte b/packages/kit/test/apps/basics/src/routes/load/serialization-post.svelte
new file mode 100644
index 000000000000..e4162fffafd7
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/serialization-post.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+a: {a}
+b: {b}