Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/young-apricots-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Account for POST bodies when serializing fetches
12 changes: 10 additions & 2 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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));
Expand Down
13 changes: 13 additions & 0 deletions packages/kit/src/runtime/hash.js
Original file line number Diff line number Diff line change
@@ -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);
}
27 changes: 18 additions & 9 deletions packages/kit/src/runtime/server/page/load_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function load_node({

/** @type {Array<{
* url: string;
* body: string;
* json: string;
* }>} */
const fetched = [];
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
9 changes: 7 additions & 2 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import devalue from 'devalue';
import { writable } from 'svelte/store';
import { hash } from '../../hash.js';

const s = JSON.stringify;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -156,7 +157,11 @@ export async function render_response({
: `${rendered.html}

${serialized_data
.map(({ url, json }) => `<script type="svelte-data" url="${url}">${json}</script>`)
.map(({ url, body, json }) => {
return body
? `<script type="svelte-data" url="${url}" body="${hash(body)}">${json}</script>`
: `<script type="svelte-data" url="${url}">${json}</script>`;
})
.join('\n\n\t\t\t')}
`.replace(/^\t{2}/gm, '');

Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export type Loaded = {
node: SSRNode;
loaded: NormalizedLoadOutput;
context: Record<string, any>;
fetched: Array<{ url: string; json: string }>;
fetched: Array<{ url: string; body: string; json: string }>;
uses_credentials: boolean;
};
36 changes: 35 additions & 1 deletion packages/kit/test/apps/basics/src/routes/load/_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
});
Expand All @@ -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');
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('@sveltejs/kit').RequestHandler<any, string>} */
export function post(request) {
return {
body: request.body.toUpperCase()
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script context="module">
/** @type {import('@sveltejs/kit').Load} */
export async function load({ fetch }) {
/** @param {string} body */
async function post(body) {
const res = await fetch('/load/serialization-post.json', {
method: 'POST',
headers: {
'content-type': 'text/plain'
},
body
});

return await res.text();
}
const a = await post('x');
const b = await post('y');

return {
props: { a, b }
};
}
</script>

<script>
/** @type {string} */
export let a;

/** @type {string} */
export let b;
</script>

<h1>a: {a}</h1>
<h2>b: {b}</h2>