Skip to content

Commit d62ed39

Browse files
authored
fix: skip calling respond for server-side fetch on prerendered pages (#13377)
When using server-side fetch for internal requests, if a server route is matched from the server `manifest.js`, it gets called without making a real HTTP request. However, prerendered routes are not included on this list! This is fine when routes are prerendered with `export const prerender = true;`, but will cause issues with a non-prerendered route also matches the same URL as any prerendered routes. This commit adds `prerendered_routes: Set<string>` to the `manifest.js`, which skips calling the non-prerendered route. Fixes #12778 Fixes #12739
1 parent f30352f commit d62ed39

File tree

13 files changed

+92
-2
lines changed

13 files changed

+92
-2
lines changed

.changeset/nine-llamas-fetch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: skip hooks for server fetch to prerendered routes

.changeset/slow-penguins-play.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: default server fetch to use prerendered paths

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

+2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export function create_builder({
138138
generateManifest: ({ relativePath }) =>
139139
generate_manifest({
140140
build_data,
141+
prerendered: [],
141142
relative_path: relativePath,
142143
routes: Array.from(filtered)
143144
})
@@ -185,6 +186,7 @@ export function create_builder({
185186
generateManifest({ relativePath, routes: subset }) {
186187
return generate_manifest({
187188
build_data,
189+
prerendered: prerendered.paths,
188190
relative_path: relativePath,
189191
routes: subset
190192
? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route)))

packages/kit/src/core/generate_manifest/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import { find_server_assets } from './find_server_assets.js';
1414
* build process, to power routing, etc.
1515
* @param {{
1616
* build_data: import('types').BuildData;
17+
* prerendered: string[];
1718
* relative_path: string;
1819
* routes: import('types').RouteData[];
1920
* }} opts
2021
*/
21-
export function generate_manifest({ build_data, relative_path, routes }) {
22+
export function generate_manifest({ build_data, prerendered, relative_path, routes }) {
2223
/**
2324
* @type {Map<any, number>} The new index of each node in the filtered nodes array
2425
*/
@@ -113,6 +114,7 @@ export function generate_manifest({ build_data, relative_path, routes }) {
113114
`;
114115
}).filter(Boolean).join(',\n')}
115116
],
117+
prerendered_routes: new Set(${s(prerendered)}),
116118
matchers: async () => {
117119
${Array.from(
118120
matchers,

packages/kit/src/exports/public.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,7 @@ export interface SSRManifest {
12971297
client: NonNullable<BuildData['client']>;
12981298
nodes: SSRNodeLoader[];
12991299
routes: SSRRoute[];
1300+
prerendered_routes: Set<string>;
13001301
matchers: () => Promise<Record<string, ParamMatcher>>;
13011302
/** A `[file]: size` map of all assets imported by server code */
13021303
server_assets: Record<string, number>;

packages/kit/src/exports/vite/dev/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ export async function dev(vite, vite_config, svelte_config) {
222222
return result;
223223
};
224224
}),
225+
prerendered_routes: new Set(),
225226
routes: compact(
226227
manifest_data.routes.map((route) => {
227228
if (!route.page && !route.endpoint) return null;

packages/kit/src/exports/vite/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,7 @@ Tips:
795795
manifest_path,
796796
`export const manifest = ${generate_manifest({
797797
build_data,
798+
prerendered: [],
798799
relative_path: '.',
799800
routes: manifest_data.routes
800801
})};\n`
@@ -917,6 +918,7 @@ Tips:
917918
manifest_path,
918919
`export const manifest = ${generate_manifest({
919920
build_data,
921+
prerendered: [],
920922
relative_path: '.',
921923
routes: manifest_data.routes
922924
})};\n`
@@ -948,6 +950,7 @@ Tips:
948950
`${out}/server/manifest.js`,
949951
`export const manifest = ${generate_manifest({
950952
build_data,
953+
prerendered: prerendered.paths,
951954
relative_path: '.',
952955
routes: manifest_data.routes.filter((route) => prerender_map.get(route.id) !== true)
953956
})};\n`

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

+12
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
112112
return await fetch(request);
113113
}
114114

115+
if (
116+
manifest._.prerendered_routes.has(decoded) ||
117+
(decoded.at(-1) === '/' && manifest._.prerendered_routes.has(decoded.slice(0, -1)))
118+
) {
119+
// The path of something prerendered could match a different route
120+
// that is still in the manifest, leading to the wrong route being loaded.
121+
// We therefore bail early here. The prerendered logic is different for
122+
// each adapter, (except maybe for prerendered redirects)
123+
// so we need to make an actual HTTP request.
124+
return await fetch(request);
125+
}
126+
115127
if (credentials !== 'omit') {
116128
const cookie = get_cookie_header(url, request.headers.get('cookie'));
117129
if (cookie) {

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

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { building, dev } from '$app/environment';
12
import { error, isHttpError, redirect } from '@sveltejs/kit';
23
import { sequence } from '@sveltejs/kit/hooks';
34
import fs from 'node:fs';
@@ -136,6 +137,15 @@ export const handle = sequence(
136137

137138
return resolve(event);
138139
},
140+
async ({ event, resolve }) => {
141+
if (!dev && !building && event.url.pathname === '/prerendering/prerendered-endpoint/api') {
142+
error(
143+
500,
144+
`Server hooks should not be called for prerendered endpoints: isSubRequest=${event.isSubRequest}`
145+
);
146+
}
147+
return resolve(event);
148+
},
139149
async ({ event, resolve }) => {
140150
if (['/non-existent-route', '/non-existent-route-loop'].includes(event.url.pathname)) {
141151
event.locals.url = new URL(event.request.url);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { building, dev } from '$app/environment';
2+
import { error, json } from '@sveltejs/kit';
3+
4+
export const prerender = 'auto';
5+
6+
export function entries() {
7+
return [
8+
{
9+
option: 'prerendered'
10+
}
11+
];
12+
}
13+
14+
export async function GET({ params: { option } }) {
15+
if ((await entries()).find((entry) => entry.option === option)) {
16+
if (dev || building) {
17+
return json({ message: 'Im prerendered and called from a non-prerendered +page.server.js' });
18+
} else {
19+
error(500, 'I should not be called at runtime because I am prerendered');
20+
}
21+
}
22+
return json({ message: 'Im not prerendered' });
23+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
export async function GET({ fetch }) {
1+
export async function GET({ fetch, url }) {
2+
if (url.searchParams.get('api-with-param-option') === 'prerendered') {
3+
return await fetch('/prerendering/prerendered-endpoint/api-with-param/prerendered');
4+
}
5+
26
return await fetch('/prerendering/prerendered-endpoint/api');
37
}

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

+21
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ test.describe('Endpoints', () => {
121121
});
122122
});
123123

124+
test('Partially Prerendered +server.js called from a non-prerendered +server.js works', async ({
125+
baseURL
126+
}) => {
127+
for (const [description, url] of [
128+
['direct', `${baseURL}/prerendering/prerendered-endpoint/api-with-param/prerendered`],
129+
[
130+
'proxied',
131+
`${baseURL}/prerendering/prerendered-endpoint/proxy?api-with-param-option=prerendered`
132+
]
133+
]) {
134+
await test.step(description, async () => {
135+
const res = await fetch(url);
136+
137+
expect(res.status).toBe(200);
138+
expect(await res.json()).toStrictEqual({
139+
message: 'Im prerendered and called from a non-prerendered +page.server.js'
140+
});
141+
});
142+
}
143+
});
144+
124145
test('invalid request method returns allow header', async ({ request }) => {
125146
const response = await request.post('/endpoint-output/body');
126147

packages/kit/types/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,7 @@ declare module '@sveltejs/kit' {
12791279
client: NonNullable<BuildData['client']>;
12801280
nodes: SSRNodeLoader[];
12811281
routes: SSRRoute[];
1282+
prerendered_routes: Set<string>;
12821283
matchers: () => Promise<Record<string, ParamMatcher>>;
12831284
/** A `[file]: size` map of all assets imported by server code */
12841285
server_assets: Record<string, number>;

0 commit comments

Comments
 (0)