Skip to content

Commit 2dc7774

Browse files
[fix] reading from same response body twice during prerender (#3473) (#3521)
* Add failing test for #3473 prerender error body used already * Fix reading from same response body twice during prerender * Fix quotes * Add changeset * Fix casing for internal variables * Revert change of cloning response as bug was actually in prerender.js * Avoid reading response body twice * Revert "Avoid reading response body twice" This reverts commit cecf7dd. Revert being stupid #1 * Revert "Revert change of cloning response as bug was actually in prerender.js" This reverts commit 5e5f30b. Revert being stupid #2 * store buffered depenedency bodies for prerendering * failing test for non-buffered endpoint data * use buffered body if available, otherwise buffer Co-authored-by: Rich Harris <[email protected]>
1 parent b3d3f5c commit 2dc7774

File tree

9 files changed

+116
-23
lines changed

9 files changed

+116
-23
lines changed

.changeset/nine-dots-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[fix] reading from same response body twice during prerender (#3473)

packages/kit/src/core/adapt/prerender/prerender.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,9 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
130130
* @param {string?} referrer
131131
*/
132132
async function visit(path, decoded_path, referrer) {
133-
/** @type {Map<string, Response>} */
133+
/** @type {Map<string, import('types/internal').PrerenderDependency>} */
134134
const dependencies = new Map();
135+
135136
const render_path = config.kit.paths?.base
136137
? `http://sveltekit-prerender${config.kit.paths.base}${path === '/' ? '' : path}`
137138
: `http://sveltekit-prerender${path}`;
@@ -192,9 +193,11 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
192193
}
193194

194195
for (const [dependency_path, result] of dependencies) {
195-
const response_type = Math.floor(result.status / 100);
196+
const { status, headers } = result.response;
197+
198+
const response_type = Math.floor(status / 100);
196199

197-
const is_html = result.headers.get('content-type') === 'text/html';
200+
const is_html = headers.get('content-type') === 'text/html';
198201

199202
const parts = dependency_path.split('/');
200203
if (is_html && parts[parts.length - 1] !== 'index.html') {
@@ -204,16 +207,17 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
204207
const file = `${out}${parts.join('/')}`;
205208
mkdirp(dirname(file));
206209

207-
if (result.body) {
208-
writeFileSync(file, await result.text());
209-
paths.push(dependency_path);
210-
}
210+
writeFileSync(
211+
file,
212+
result.body === null ? new Uint8Array(await result.response.arrayBuffer()) : result.body
213+
);
214+
paths.push(dependency_path);
211215

212216
if (response_type === OK) {
213-
log.info(`${result.status} ${dependency_path}`);
217+
log.info(`${status} ${dependency_path}`);
214218
} else {
215219
error({
216-
status: result.status,
220+
status,
217221
path: dependency_path,
218222
referrer: path,
219223
referenceType: 'fetched'

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ export async function load_node({
9999
/** @type {Response} */
100100
let response;
101101

102+
/** @type {import('types/internal').PrerenderDependency} */
103+
let dependency;
104+
102105
// handle fetch requests for static assets. e.g. prebaked data, etc.
103106
// we need to support everything the browser's fetch supports
104107
const prefix = options.paths.assets || options.paths.base;
@@ -125,8 +128,6 @@ export async function load_node({
125128
response = await fetch(`${url.origin}/${file}`, /** @type {RequestInit} */ (opts));
126129
}
127130
} else if (is_root_relative(resolved)) {
128-
const relative = resolved;
129-
130131
if (opts.credentials !== 'omit') {
131132
uses_credentials = true;
132133

@@ -150,20 +151,15 @@ export async function load_node({
150151
throw new Error('Request body must be a string');
151152
}
152153

153-
const rendered = await respond(
154-
new Request(new URL(requested, event.url).href, opts),
155-
options,
156-
{
157-
fetched: requested,
158-
initiator: route
159-
}
160-
);
154+
response = await respond(new Request(new URL(requested, event.url).href, opts), options, {
155+
fetched: requested,
156+
initiator: route
157+
});
161158

162159
if (state.prerender) {
163-
state.prerender.dependencies.set(relative, rendered);
160+
dependency = { response, body: null };
161+
state.prerender.dependencies.set(resolved, dependency);
164162
}
165-
166-
response = rendered;
167163
} else {
168164
// external
169165
if (resolved.startsWith('//')) {
@@ -219,9 +215,28 @@ export async function load_node({
219215
});
220216
}
221217

218+
if (dependency) {
219+
dependency.body = body;
220+
}
221+
222222
return body;
223223
}
224224

225+
if (key === 'arrayBuffer') {
226+
return async () => {
227+
const buffer = await response.arrayBuffer();
228+
229+
if (dependency) {
230+
dependency.body = new Uint8Array(buffer);
231+
}
232+
233+
// TODO should buffer be inlined into the page (albeit base64'd)?
234+
// any conditions in which it shouldn't be?
235+
236+
return buffer;
237+
};
238+
}
239+
225240
if (key === 'text') {
226241
return text;
227242
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export async function get() {
2+
return {
3+
body: { answer: 42 }
4+
};
5+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script context="module">
2+
/** @type {import('@sveltejs/kit').Load} */
3+
export async function load({ fetch }) {
4+
const url = '/fetch-endpoint/buffered.json';
5+
const res = await fetch(url);
6+
7+
return {
8+
props: await res.json()
9+
};
10+
}
11+
</script>
12+
13+
<script>
14+
/** @type {number} */
15+
export let answer;
16+
</script>
17+
18+
<h1>the answer is {answer}</h1>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export async function get() {
2+
return {
3+
body: { answer: 42 }
4+
};
5+
}
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('@sveltejs/kit').Load} */
3+
export async function load({ fetch }) {
4+
const url = '/fetch-endpoint/not-buffered.json';
5+
const res = await fetch(url);
6+
7+
return {
8+
props: {
9+
headers: res.headers
10+
}
11+
};
12+
}
13+
</script>
14+
15+
<script>
16+
/** @type {Headers} */
17+
export let headers;
18+
</script>
19+
20+
<h1>content-type: {headers.get('content-type')}</h1>

packages/kit/test/prerendering/basics/test/test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,20 @@ test('inserts http-equiv tag for cache-control headers', () => {
4242
assert.ok(content.includes('<meta http-equiv="cache-control" content="max-age=300">'));
4343
});
4444

45+
test('renders page with data from endpoint', () => {
46+
const content = read('fetch-endpoint/buffered/index.html');
47+
assert.ok(content.includes('<h1>the answer is 42</h1>'));
48+
49+
const json = read('fetch-endpoint/buffered.json');
50+
assert.equal(json, JSON.stringify({ answer: 42 }));
51+
});
52+
53+
test('renders page with unbuffered data from endpoint', () => {
54+
const content = read('fetch-endpoint/not-buffered/index.html');
55+
assert.ok(content.includes('<h1>content-type: application/json; charset=utf-8</h1>'), content);
56+
57+
const json = read('fetch-endpoint/not-buffered.json');
58+
assert.equal(json, JSON.stringify({ answer: 42 }));
59+
});
60+
4561
test.run();

packages/kit/types/internal.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ import { Either } from './helper';
55
import { ExternalFetch, GetSession, Handle, HandleError, RequestEvent } from './hooks';
66
import { Load } from './page';
77

8+
export interface PrerenderDependency {
9+
response: Response;
10+
body: null | string | Uint8Array;
11+
}
12+
813
export interface PrerenderOptions {
914
fallback?: string;
1015
all: boolean;
11-
dependencies: Map<string, Response>;
16+
dependencies: Map<string, PrerenderDependency>;
1217
}
1318

1419
export interface AppModule {

0 commit comments

Comments
 (0)