Skip to content

Commit 531cf0c

Browse files
author
Rich Harris
authored
Use a proxy to intercept responses, rather than a clone (#635)
* add failing test for #591 * use a proxy to intercept responses
1 parent b99f917 commit 531cf0c

File tree

4 files changed

+121
-15
lines changed

4 files changed

+121
-15
lines changed

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

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { parse, resolve, URLSearchParams } from 'url';
55
import { normalize } from '../load.js';
66
import { ssr } from './index.js';
77

8+
const s = JSON.stringify;
9+
810
/**
911
* @param {{
1012
* request: import('types.internal').Request;
@@ -26,7 +28,15 @@ async function get_response({ request, options, $session, route, status = 200, e
2628
throw new Error(`Failed to serialize session data: ${error.message}`);
2729
});
2830

29-
/** @type {Array<{ url: string, payload: string }>} */
31+
/** @type {Array<{
32+
* url: string;
33+
* payload: {
34+
* status: number;
35+
* statusText: string;
36+
* headers: import('types.internal').Headers;
37+
* body: string;
38+
* }
39+
* }>} */
3040
const serialized_data = [];
3141

3242
const match = route && route.pattern.exec(request.path);
@@ -148,25 +158,51 @@ async function get_response({ request, options, $session, route, status = 200, e
148158
}
149159

150160
if (response) {
151-
const clone = response.clone();
152-
153161
/** @type {import('types.internal').Headers} */
154162
const headers = {};
155-
clone.headers.forEach((value, key) => {
163+
response.headers.forEach((value, key) => {
156164
if (key !== 'etag') headers[key] = value;
157165
});
158166

159-
const payload = JSON.stringify({
160-
status: clone.status,
161-
statusText: clone.statusText,
162-
headers,
163-
body: await clone.text() // TODO handle binary data
164-
});
167+
const inline = {
168+
url,
169+
payload: {
170+
status: response.status,
171+
statusText: response.statusText,
172+
headers,
165173

166-
// TODO i guess we need to sanitize/escape this... somehow?
167-
serialized_data.push({ url, payload });
174+
/** @type {string} */
175+
body: null
176+
}
177+
};
178+
179+
const proxy = new Proxy(response, {
180+
get(response, key, receiver) {
181+
if (key === 'text') {
182+
return async () => {
183+
const text = await response.text();
184+
inline.payload.body = text;
185+
serialized_data.push(inline);
186+
return text;
187+
};
188+
}
189+
190+
if (key === 'json') {
191+
return async () => {
192+
const json = await response.json();
193+
inline.payload.body = s(json);
194+
serialized_data.push(inline);
195+
return json;
196+
};
197+
}
198+
199+
// TODO arrayBuffer?
200+
201+
return Reflect.get(response, key, receiver);
202+
}
203+
});
168204

169-
return response;
205+
return proxy;
170206
}
171207

172208
return new Response('Not found', {
@@ -327,7 +363,6 @@ async function get_response({ request, options, $session, route, status = 200, e
327363
const css_deps = route ? route.css : [];
328364
const style = route ? route.style : '';
329365

330-
const s = JSON.stringify;
331366
const prefix = `${options.paths.assets}/${options.app_dir}`;
332367

333368
// 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
380415
: `${rendered.html}
381416
382417
${serialized_data
383-
.map(({ url, payload }) => `<script type="svelte-data" url="${url}">${payload}</script>`)
418+
.map(({ url, payload }) => `<script type="svelte-data" url="${url}">${s(payload)}</script>`)
384419
.join('\n\n\t\t\t')}
385420
`.replace(/^\t{2}/gm, '');
386421

packages/kit/test/apps/basics/src/routes/load/__tests__.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import http from 'http';
2+
import * as ports from 'port-authority';
13
import * as assert from 'uvu/assert';
24

35
/** @type {import('../../../../../types').TestMaker} */
@@ -83,4 +85,52 @@ export default function (test, is_dev) {
8385
await clicknav('[href="/load/fetch-request"]');
8486
assert.equal(await page.textContent('h1'), 'the answer is 42');
8587
});
88+
89+
test('handles large responses', '/load', async ({ base, page }) => {
90+
const port = await ports.find(4000);
91+
92+
const chunk_size = 50000;
93+
const chunk_count = 100;
94+
const total_size = chunk_size * chunk_count;
95+
96+
let chunk = '';
97+
for (let i = 0; i < chunk_size; i += 1) {
98+
chunk += String(i % 10);
99+
}
100+
101+
let times_responded = 0;
102+
103+
const server = http.createServer(async (req, res) => {
104+
if (req.url === '/large-response.json') {
105+
times_responded += 1;
106+
107+
res.writeHead(200, {
108+
'Access-Control-Allow-Origin': '*'
109+
});
110+
111+
for (let i = 0; i < chunk_count; i += 1) {
112+
if (!res.write(chunk)) {
113+
await new Promise((fulfil) => {
114+
res.once('drain', () => {
115+
fulfil();
116+
});
117+
});
118+
}
119+
}
120+
121+
res.end();
122+
}
123+
});
124+
125+
await new Promise((fulfil) => {
126+
server.listen(port, () => fulfil());
127+
});
128+
129+
await page.goto(`${base}/load/large-response?port=${port}`);
130+
assert.equal(await page.textContent('h1'), `text.length is ${total_size}`);
131+
132+
assert.equal(times_responded, 1);
133+
134+
server.close();
135+
});
86136
}

packages/kit/test/apps/basics/src/routes/load/index.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
<h1>bar == {foo}?</h1>
2020

2121
<a href="/load/fetch-request">fetch request</a>
22+
<a href="/load/large-response">large response</a>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script context="module">
2+
/** @type {import('../../../../../../types').Load} */
3+
export async function load({ page, fetch }) {
4+
const res = await fetch(`http://localhost:${page.query.get('port')}/large-response.json`)
5+
const text = await res.text();
6+
7+
return {
8+
props: {
9+
text
10+
}
11+
};
12+
}
13+
</script>
14+
15+
<script>
16+
/** @type {string} */
17+
export let text;
18+
</script>
19+
20+
<h1>text.length is {text.length}</h1>

0 commit comments

Comments
 (0)