Skip to content

Commit 533c242

Browse files
authored
Add middleware prefetching config (vercel#42936)
This adds a new `experimental.middlewarePrefetch` config with two modes with the default being the `flexible` config. - `strict` only prefetches when the `href` explicitly matches an SSG route (won't prefetch for middleware rewrite usage unless manual `href`/`as` values are used) - `flexible` always prefetches ensuring middleware rewrite usage is handled and also prevents executing SSR routes during prefetch to avoid unexpected invocations x-ref: vercel#39920 x-ref: [slack thread](https://vercel.slack.com/archives/C047HMFN58X/p1668473101696689?thread_ts=1667856323.709179&cid=C047HMFN58X) ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
1 parent 07d3da1 commit 533c242

File tree

11 files changed

+202
-47
lines changed

11 files changed

+202
-47
lines changed

packages/next/build/webpack-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ export function getDefineEnv({
229229
'process.env.__NEXT_OPTIMISTIC_CLIENT_CACHE': JSON.stringify(
230230
config.experimental.optimisticClientCache
231231
),
232+
'process.env.__NEXT_MIDDLEWARE_PREFETCH': JSON.stringify(
233+
config.experimental.middlewarePrefetch
234+
),
232235
'process.env.__NEXT_CROSS_ORIGIN': JSON.stringify(config.crossOrigin),
233236
'process.browser': JSON.stringify(isClient),
234237
'process.env.__NEXT_TEST_MODE': JSON.stringify(

packages/next/server/base-server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,15 @@ export default abstract class Server<ServerOptions extends Options = Options> {
10541054
) &&
10551055
(isSSG || hasServerProps)
10561056

1057+
// when we are handling a middleware prefetch and it doesn't
1058+
// resolve to a static data route we bail early to avoid
1059+
// unexpected SSR invocations
1060+
if (!isSSG && req.headers['x-middleware-prefetch']) {
1061+
res.setHeader('x-middleware-skip', '1')
1062+
res.body('{}').send()
1063+
return null
1064+
}
1065+
10571066
if (isAppPath) {
10581067
res.setHeader('vary', RSC_VARY_HEADER)
10591068

packages/next/server/config-schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ const configSchema = {
304304
manualClientBasePath: {
305305
type: 'boolean',
306306
},
307+
middlewarePrefetch: {
308+
// automatic typing doesn't like enum
309+
enum: ['strict', 'flexible'] as any,
310+
type: 'string',
311+
},
307312
modularizeImports: {
308313
type: 'object',
309314
},

packages/next/server/config-shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface ExperimentalConfig {
8383
skipMiddlewareUrlNormalize?: boolean
8484
skipTrailingSlashRedirect?: boolean
8585
optimisticClientCache?: boolean
86+
middlewarePrefetch?: 'strict' | 'flexible'
8687
legacyBrowsers?: boolean
8788
manualClientBasePath?: boolean
8889
newNextLinkBehavior?: boolean
@@ -563,6 +564,7 @@ export const defaultConfig: NextConfig = {
563564
swcMinify: true,
564565
output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined,
565566
experimental: {
567+
middlewarePrefetch: 'flexible',
566568
optimisticClientCache: true,
567569
runtime: undefined,
568570
manualClientBasePath: false,

packages/next/server/next-server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,6 +1975,14 @@ export default class NextNodeServer extends BaseServer {
19751975
? `${parsedDestination.hostname}:${parsedDestination.port}`
19761976
: parsedDestination.hostname) !== req.headers.host
19771977
) {
1978+
// when we are handling a middleware prefetch and it doesn't
1979+
// resolve to a static data route we bail early to avoid
1980+
// unexpected SSR invocations
1981+
if (req.headers['x-middleware-prefetch']) {
1982+
res.setHeader('x-middleware-skip', '1')
1983+
res.body('{}').send()
1984+
return { finished: true }
1985+
}
19781986
return this.proxyRequest(
19791987
req as NodeNextRequest,
19801988
res as NodeNextResponse,

packages/next/shared/lib/router/router.ts

Lines changed: 106 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -635,8 +635,6 @@ function fetchRetry(
635635
})
636636
}
637637

638-
const backgroundCache: Record<string, Promise<any>> = {}
639-
640638
interface FetchDataOutput {
641639
dataHref: string
642640
json: Record<string, any> | null
@@ -687,7 +685,11 @@ function fetchNextData({
687685
const { href: cacheKey } = new URL(dataHref, window.location.href)
688686
const getData = (params?: { method?: 'HEAD' | 'GET' }) =>
689687
fetchRetry(dataHref, isServerRender ? 3 : 1, {
690-
headers: isPrefetch ? { purpose: 'prefetch' } : {},
688+
headers: Object.assign(
689+
{} as HeadersInit,
690+
isPrefetch ? { purpose: 'prefetch' } : {},
691+
isPrefetch && hasMiddleware ? { 'x-middleware-prefetch': '1' } : {}
692+
),
691693
method: params?.method ?? 'GET',
692694
})
693695
.then((response) => {
@@ -756,7 +758,12 @@ function fetchNextData({
756758
return data
757759
})
758760
.catch((err) => {
759-
delete inflightCache[cacheKey]
761+
if (!unstable_skipClientCache) {
762+
delete inflightCache[cacheKey]
763+
}
764+
if (err.message === 'Failed to fetch') {
765+
markAssetError(err)
766+
}
760767
throw err
761768
})
762769

@@ -839,8 +846,10 @@ export default class Router implements BaseRouter {
839846
* Map of all components loaded in `Router`
840847
*/
841848
components: { [pathname: string]: PrivateRouteInfo }
842-
// Server Data Cache
849+
// Server Data Cache (full data requests)
843850
sdc: NextDataCache = {}
851+
// Server Background Cache (HEAD requests)
852+
sbc: NextDataCache = {}
844853

845854
sub: Subscription
846855
clc: ComponentLoadCancel
@@ -1966,6 +1975,7 @@ export default class Router implements BaseRouter {
19661975
? existingInfo
19671976
: undefined
19681977

1978+
const isBackground = isQueryUpdating
19691979
const fetchNextDataParams: FetchNextDataParams = {
19701980
dataHref: this.pageLoader.getDataHref({
19711981
href: formatWithValidation({ pathname, query }),
@@ -1976,11 +1986,11 @@ export default class Router implements BaseRouter {
19761986
hasMiddleware: true,
19771987
isServerRender: this.isSsr,
19781988
parseJSON: true,
1979-
inflightCache: this.sdc,
1989+
inflightCache: isBackground ? this.sbc : this.sdc,
19801990
persistCache: !isPreview,
19811991
isPrefetch: false,
19821992
unstable_skipClientCache,
1983-
isBackground: isQueryUpdating,
1993+
isBackground,
19841994
}
19851995

19861996
const data =
@@ -2071,26 +2081,36 @@ export default class Router implements BaseRouter {
20712081
)
20722082
}
20732083
}
2084+
const wasBailedPrefetch = data?.response?.headers.get('x-middleware-skip')
20742085

20752086
const shouldFetchData = routeInfo.__N_SSG || routeInfo.__N_SSP
20762087

2088+
// For non-SSG prefetches that bailed before sending data
2089+
// we clear the cache to fetch full response
2090+
if (wasBailedPrefetch) {
2091+
delete this.sdc[data?.dataHref]
2092+
}
2093+
20772094
const { props, cacheKey } = await this._getData(async () => {
20782095
if (shouldFetchData) {
2079-
const { json, cacheKey: _cacheKey } = data?.json
2080-
? data
2081-
: await fetchNextData({
2082-
dataHref: this.pageLoader.getDataHref({
2083-
href: formatWithValidation({ pathname, query }),
2084-
asPath: resolvedAs,
2085-
locale,
2086-
}),
2087-
isServerRender: this.isSsr,
2088-
parseJSON: true,
2089-
inflightCache: this.sdc,
2090-
persistCache: !isPreview,
2091-
isPrefetch: false,
2092-
unstable_skipClientCache,
2093-
})
2096+
const { json, cacheKey: _cacheKey } =
2097+
data?.json && !wasBailedPrefetch
2098+
? data
2099+
: await fetchNextData({
2100+
dataHref:
2101+
data?.dataHref ||
2102+
this.pageLoader.getDataHref({
2103+
href: formatWithValidation({ pathname, query }),
2104+
asPath: resolvedAs,
2105+
locale,
2106+
}),
2107+
isServerRender: this.isSsr,
2108+
parseJSON: true,
2109+
inflightCache: wasBailedPrefetch ? {} : this.sdc,
2110+
persistCache: !isPreview,
2111+
isPrefetch: false,
2112+
unstable_skipClientCache,
2113+
})
20942114

20952115
return {
20962116
cacheKey: _cacheKey,
@@ -2135,7 +2155,7 @@ export default class Router implements BaseRouter {
21352155
Object.assign({}, fetchNextDataParams, {
21362156
isBackground: true,
21372157
persistCache: false,
2138-
inflightCache: backgroundCache,
2158+
inflightCache: this.sbc,
21392159
})
21402160
).catch(() => {})
21412161
}
@@ -2278,6 +2298,12 @@ export default class Router implements BaseRouter {
22782298
? options.locale || undefined
22792299
: this.locale
22802300

2301+
const isMiddlewareMatch = await matchesMiddleware({
2302+
asPath: asPath,
2303+
locale: locale,
2304+
router: this,
2305+
})
2306+
22812307
if (process.env.__NEXT_HAS_REWRITES && asPath.startsWith('/')) {
22822308
let rewrites: any
22832309
;({ __rewrites: rewrites } = await getClientBuildManifest())
@@ -2305,7 +2331,9 @@ export default class Router implements BaseRouter {
23052331
pathname = rewritesResult.resolvedHref
23062332
parsed.pathname = pathname
23072333

2308-
url = formatWithValidation(parsed)
2334+
if (!isMiddlewareMatch) {
2335+
url = formatWithValidation(parsed)
2336+
}
23092337
}
23102338
}
23112339
parsed.pathname = resolveDynamicRoute(parsed.pathname, pages)
@@ -2320,25 +2348,73 @@ export default class Router implements BaseRouter {
23202348
) || {}
23212349
)
23222350

2323-
url = formatWithValidation(parsed)
2351+
if (!isMiddlewareMatch) {
2352+
url = formatWithValidation(parsed)
2353+
}
23242354
}
23252355

23262356
// Prefetch is not supported in development mode because it would trigger on-demand-entries
23272357
if (process.env.NODE_ENV !== 'production') {
23282358
return
23292359
}
23302360

2361+
const data =
2362+
process.env.__NEXT_MIDDLEWARE_PREFETCH === 'strict'
2363+
? ({} as any)
2364+
: await withMiddlewareEffects({
2365+
fetchData: () =>
2366+
fetchNextData({
2367+
dataHref: this.pageLoader.getDataHref({
2368+
href: formatWithValidation({ pathname, query }),
2369+
skipInterpolation: true,
2370+
asPath: resolvedAs,
2371+
locale,
2372+
}),
2373+
hasMiddleware: true,
2374+
isServerRender: this.isSsr,
2375+
parseJSON: true,
2376+
inflightCache: this.sdc,
2377+
persistCache: !this.isPreview,
2378+
isPrefetch: true,
2379+
}),
2380+
asPath: asPath,
2381+
locale: locale,
2382+
router: this,
2383+
})
2384+
2385+
/**
2386+
* If there was a rewrite we apply the effects of the rewrite on the
2387+
* current parameters for the prefetch.
2388+
*/
2389+
if (data?.effect.type === 'rewrite') {
2390+
parsed.pathname = data.effect.resolvedHref
2391+
pathname = data.effect.resolvedHref
2392+
query = { ...query, ...data.effect.parsedAs.query }
2393+
resolvedAs = data.effect.parsedAs.pathname
2394+
url = formatWithValidation(parsed)
2395+
}
2396+
2397+
/**
2398+
* If there is a redirect to an external destination then we don't have
2399+
* to prefetch content as it will be unused.
2400+
*/
2401+
if (data?.effect.type === 'redirect-external') {
2402+
return
2403+
}
2404+
23312405
const route = removeTrailingSlash(pathname)
23322406

23332407
await Promise.all([
23342408
this.pageLoader._isSsg(route).then((isSsg) => {
23352409
return isSsg
23362410
? fetchNextData({
2337-
dataHref: this.pageLoader.getDataHref({
2338-
href: url,
2339-
asPath: resolvedAs,
2340-
locale: locale,
2341-
}),
2411+
dataHref:
2412+
data?.dataHref ||
2413+
this.pageLoader.getDataHref({
2414+
href: url,
2415+
asPath: resolvedAs,
2416+
locale: locale,
2417+
}),
23422418
isServerRender: false,
23432419
parseJSON: true,
23442420
inflightCache: this.sdc,

test/e2e/middleware-rewrites/app/pages/about.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
export default function Main({ message, middleware }) {
1+
export default function Main({ message, middleware, now }) {
22
return (
33
<div>
44
<h1 className="title">About Page</h1>
55
<p className={message}>{message}</p>
66
<p className="middleware">{middleware}</p>
7+
<p className="now">{now}</p>
78
</div>
89
)
910
}
1011

1112
export const getServerSideProps = ({ query }) => ({
1213
props: {
14+
now: Date.now(),
1315
middleware: query.middleware || '',
1416
message: query.message || '',
1517
},

test/e2e/middleware-rewrites/app/pages/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ export default function Home() {
5151
Rewrite me to internal path
5252
</Link>
5353
<div />
54+
<Link href="/rewrite-to-static" id="rewrite-to-static">
55+
Rewrite me to static
56+
</Link>
57+
<div />
58+
<Link href="/fallback-true-blog/rewritten" id="rewrite-to-ssr">
59+
Rewrite me to /about (SSR)
60+
</Link>
61+
<div />
5462
<Link href="/ssg" id="normal-ssg-link">
5563
normal SSG link
5664
</Link>

test/e2e/middleware-rewrites/app/pages/static-ssg/[slug].js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,19 @@ export default function Page() {
1212
</>
1313
)
1414
}
15+
16+
export function getStaticPaths() {
17+
return {
18+
paths: ['/static-ssg/first'],
19+
fallback: 'blocking',
20+
}
21+
}
22+
23+
export function getStaticProps({ params }) {
24+
return {
25+
props: {
26+
now: Date.now(),
27+
params,
28+
},
29+
}
30+
}

0 commit comments

Comments
 (0)