Skip to content

Commit c9198a3

Browse files
authored
fix: allow non-prerendered API endpoint calls during reroute when prerendering (#13616)
* fix: allow non-prerendered API endpoint calls during reroute when prerendering * test * tweak * fix other prerendering bug * lint
1 parent 4261e28 commit c9198a3

File tree

11 files changed

+78
-9
lines changed

11 files changed

+78
-9
lines changed

.changeset/khaki-buttons-add.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: allow non-prerendered API endpoint calls during reroute when prerendering

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export async function render_endpoint(event, mod, state) {
2929
throw new Error('Cannot prerender endpoints that have mutative methods');
3030
}
3131

32-
if (state.prerendering && !prerender) {
32+
if (state.prerendering && !state.prerendering.inside_reroute && !prerender) {
3333
if (state.depth > 0) {
3434
// if request came from a prerendered page, bail
3535
throw new Error(`${event.route.id} is not prerenderable`);
@@ -41,7 +41,7 @@ export async function render_endpoint(event, mod, state) {
4141
}
4242

4343
try {
44-
let response = await with_event(event, () =>
44+
const response = await with_event(event, () =>
4545
handler(/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event))
4646
);
4747

@@ -51,15 +51,28 @@ export async function render_endpoint(event, mod, state) {
5151
);
5252
}
5353

54-
if (state.prerendering) {
55-
// the returned Response might have immutable Headers
56-
// so we should clone them before trying to mutate them
57-
response = new Response(response.body, {
54+
if (state.prerendering && (!state.prerendering.inside_reroute || prerender)) {
55+
// The returned Response might have immutable Headers
56+
// so we should clone them before trying to mutate them.
57+
// We also need to clone the response body since it may be read twice during prerendering
58+
const cloned = new Response(response.clone().body, {
5859
status: response.status,
5960
statusText: response.statusText,
6061
headers: new Headers(response.headers)
6162
});
62-
response.headers.set('x-sveltekit-prerender', String(prerender));
63+
cloned.headers.set('x-sveltekit-prerender', String(prerender));
64+
65+
if (state.prerendering.inside_reroute && prerender) {
66+
// Without this, the route wouldn't be recorded as prerendered,
67+
// because there's nothing after reroute that would do that.
68+
cloned.headers.set(
69+
'x-sveltekit-routeid',
70+
encodeURI(/** @type {string} */ (event.route.id))
71+
);
72+
state.prerendering.dependencies.set(event.url.pathname, { response: cloned, body: null });
73+
} else {
74+
return cloned;
75+
}
6376
}
6477

6578
return response;

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,14 +183,21 @@ export async function respond(request, options, manifest, state) {
183183

184184
let resolved_path;
185185

186+
const prerendering_reroute_state = state.prerendering?.inside_reroute;
186187
try {
188+
// For the duration or a reroute, disable the prerendering state as reroute could call API endpoints
189+
// which would end up in the wrong logic path if not disabled.
190+
if (state.prerendering) state.prerendering.inside_reroute = true;
191+
187192
// reroute could alter the given URL, so we pass a copy
188193
resolved_path =
189194
(await options.hooks.reroute({ url: new URL(url), fetch: event.fetch })) ?? url.pathname;
190195
} catch {
191196
return text('Internal Server Error', {
192197
status: 500
193198
});
199+
} finally {
200+
if (state.prerendering) state.prerendering.inside_reroute = prerendering_reroute_state;
194201
}
195202

196203
try {
@@ -349,7 +356,9 @@ export async function respond(request, options, manifest, state) {
349356

350357
set_trailing_slash(trailing_slash);
351358

352-
if (state.prerendering && !state.prerendering.fallback) disable_search(url);
359+
if (state.prerendering && !state.prerendering.fallback && !state.prerendering.inside_reroute) {
360+
disable_search(url);
361+
}
353362

354363
const response = await with_event(event, () =>
355364
options.hooks.handle({

packages/kit/src/types/internal.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ export interface PrerenderOptions {
216216
cache?: string; // including this here is a bit of a hack, but it makes it easy to add <meta http-equiv>
217217
fallback?: boolean;
218218
dependencies: Map<string, PrerenderDependency>;
219+
/** True for the duration of a call to the `reroute` hook */
220+
inside_reroute?: boolean;
219221
}
220222

221223
export type RecursiveRequired<T> = {

packages/kit/test/apps/basics/src/hooks.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const reroute = ({ url, fetch }) => {
3131
return fetch('/reroute/api').then((r) => r.text());
3232
}
3333

34+
if (url.pathname === '/reroute/async/c') {
35+
return fetch('/reroute/api/prerendered').then((r) => r.text());
36+
}
37+
3438
if (url.pathname === '/reroute/prerendered/to-destination') {
3539
return '/reroute/prerendered/destination';
3640
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { text } from '@sveltejs/kit';
2+
3+
// This is inside a route with a route segment to ensure that the route is marked as prerendered
4+
// as part of reroute resolution even when no `entries` is given.
5+
export const prerender = true;
6+
7+
export function GET() {
8+
return text('/reroute/async/b');
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
<a href="/reroute/async/a">Go to url that should be rewritten</a>
2+
<a href="/reroute/async/c"
3+
>Go to url that should be rewritten and its reroute api call prerendered</a
4+
>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// through this we test that the crawler during prerendering does not fail on
2+
// reroute calls that call non-prerendered endpoints.
3+
export const prerender = true;

packages/kit/test/apps/basics/svelte.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ const config = {
2525
'/routing/prerendered/trailing-slash/never',
2626
'/routing/prerendered/trailing-slash/ignore'
2727
],
28-
handleHttpError: 'warn'
28+
handleHttpError: ({ path, message }) => {
29+
if (path.includes('/reroute/async')) {
30+
throw new Error('shouldnt error on ' + path);
31+
}
32+
33+
console.warn(message);
34+
}
2935
},
3036

3137
version: {

packages/kit/test/apps/basics/test/client.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,14 @@ test.describe('reroute', () => {
14611461
);
14621462
});
14631463

1464+
test('Apply async prerendered reroute during client side navigation', async ({ page }) => {
1465+
await page.goto('/reroute/async');
1466+
await page.click("a[href='/reroute/async/c']");
1467+
expect(await page.textContent('h1')).toContain(
1468+
'Successfully rewritten, URL should still show a: /reroute/async/c'
1469+
);
1470+
});
1471+
14641472
test('Apply reroute to prerendered page during client side navigation', async ({ page }) => {
14651473
await page.goto('/reroute/prerendered');
14661474
await page.click("a[href='/reroute/prerendered/to-destination']");

0 commit comments

Comments
 (0)