diff --git a/packages/kit/src/runtime/server/page.js b/packages/kit/src/runtime/server/page.js index 104ee43052f5..2455d97b844b 100644 --- a/packages/kit/src/runtime/server/page.js +++ b/packages/kit/src/runtime/server/page.js @@ -5,6 +5,8 @@ import { parse, resolve, URLSearchParams } from 'url'; import { normalize } from '../load.js'; import { ssr } from './index.js'; +const s = JSON.stringify; + /** * @param {{ * request: import('types.internal').Request; @@ -26,7 +28,15 @@ async function get_response({ request, options, $session, route, status = 200, e throw new Error(`Failed to serialize session data: ${error.message}`); }); - /** @type {Array<{ url: string, payload: string }>} */ + /** @type {Array<{ + * url: string; + * payload: { + * status: number; + * statusText: string; + * headers: import('types.internal').Headers; + * body: string; + * } + * }>} */ const serialized_data = []; const match = route && route.pattern.exec(request.path); @@ -148,25 +158,51 @@ async function get_response({ request, options, $session, route, status = 200, e } if (response) { - const clone = response.clone(); - /** @type {import('types.internal').Headers} */ const headers = {}; - clone.headers.forEach((value, key) => { + response.headers.forEach((value, key) => { if (key !== 'etag') headers[key] = value; }); - const payload = JSON.stringify({ - status: clone.status, - statusText: clone.statusText, - headers, - body: await clone.text() // TODO handle binary data - }); + const inline = { + url, + payload: { + status: response.status, + statusText: response.statusText, + headers, - // TODO i guess we need to sanitize/escape this... somehow? - serialized_data.push({ url, payload }); + /** @type {string} */ + body: null + } + }; + + const proxy = new Proxy(response, { + get(response, key, receiver) { + if (key === 'text') { + return async () => { + const text = await response.text(); + inline.payload.body = text; + serialized_data.push(inline); + return text; + }; + } + + if (key === 'json') { + return async () => { + const json = await response.json(); + inline.payload.body = s(json); + serialized_data.push(inline); + return json; + }; + } + + // TODO arrayBuffer? + + return Reflect.get(response, key, receiver); + } + }); - return response; + return proxy; } return new Response('Not found', { @@ -327,7 +363,6 @@ async function get_response({ request, options, $session, route, status = 200, e const css_deps = route ? route.css : []; const style = route ? route.style : ''; - const s = JSON.stringify; const prefix = `${options.paths.assets}/${options.app_dir}`; // TODO strip the AMP stuff out of the build if not relevant @@ -380,7 +415,7 @@ async function get_response({ request, options, $session, route, status = 200, e : `${rendered.html} ${serialized_data - .map(({ url, payload }) => ``) + .map(({ url, payload }) => ``) .join('\n\n\t\t\t')} `.replace(/^\t{2}/gm, ''); 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 7705295d3709..76b103b4a0f6 100644 --- a/packages/kit/test/apps/basics/src/routes/load/__tests__.js +++ b/packages/kit/test/apps/basics/src/routes/load/__tests__.js @@ -1,3 +1,5 @@ +import http from 'http'; +import * as ports from 'port-authority'; import * as assert from 'uvu/assert'; /** @type {import('../../../../../types').TestMaker} */ @@ -83,4 +85,52 @@ export default function (test, is_dev) { await clicknav('[href="/load/fetch-request"]'); assert.equal(await page.textContent('h1'), 'the answer is 42'); }); + + test('handles large responses', '/load', async ({ base, page }) => { + const port = await ports.find(4000); + + const chunk_size = 50000; + const chunk_count = 100; + const total_size = chunk_size * chunk_count; + + let chunk = ''; + for (let i = 0; i < chunk_size; i += 1) { + chunk += String(i % 10); + } + + let times_responded = 0; + + const server = http.createServer(async (req, res) => { + if (req.url === '/large-response.json') { + times_responded += 1; + + res.writeHead(200, { + 'Access-Control-Allow-Origin': '*' + }); + + for (let i = 0; i < chunk_count; i += 1) { + if (!res.write(chunk)) { + await new Promise((fulfil) => { + res.once('drain', () => { + fulfil(); + }); + }); + } + } + + res.end(); + } + }); + + await new Promise((fulfil) => { + server.listen(port, () => fulfil()); + }); + + await page.goto(`${base}/load/large-response?port=${port}`); + assert.equal(await page.textContent('h1'), `text.length is ${total_size}`); + + assert.equal(times_responded, 1); + + server.close(); + }); } diff --git a/packages/kit/test/apps/basics/src/routes/load/index.svelte b/packages/kit/test/apps/basics/src/routes/load/index.svelte index 1734d6d6eeed..8ba59dcdf1ff 100644 --- a/packages/kit/test/apps/basics/src/routes/load/index.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/index.svelte @@ -19,3 +19,4 @@