Skip to content

Commit 2da04af

Browse files
authored
Partial Pre Rendering Headers (#59447)
This fixes some of headers (and adds associated tests) for pages when PPR is enabled. Namely, the `Cache-Control` headers are now returning correctly, reflecting the non-cachability of some requests: - Requests that postpone (dynamic data is streamed after the initial static shell is streamed) - Requests for the Dynamic RSC payload Additionally, the `X-NextJS-Cache` header has been updated for better support for PPR: - Requests that postpone no longer return this header as it doesn't reflect the cache state of the request (because it streams) - Requests for the Prefetch RSC now returns the correct cache headers depending on the segment and pre-postpone state This also enables the other pathnames in the test suites 🙌🏻 Closes NEXT-1840
1 parent 42d6e30 commit 2da04af

File tree

13 files changed

+423
-164
lines changed

13 files changed

+423
-164
lines changed

packages/next/src/server/base-server.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,7 +1294,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
12941294
}
12951295

12961296
res.statusCode = Number(req.headers['x-invoke-status'])
1297-
let err = null
1297+
let err: Error | null = null
12981298

12991299
if (typeof req.headers['x-invoke-error'] === 'string') {
13001300
const invokeError = JSON.parse(
@@ -2601,7 +2601,18 @@ export default abstract class Server<ServerOptions extends Options = Options> {
26012601
return null
26022602
}
26032603

2604-
if (isSSG && !this.minimalMode) {
2604+
const didPostpone =
2605+
cacheEntry.value?.kind === 'PAGE' && !!cacheEntry.value.postponed
2606+
2607+
if (
2608+
isSSG &&
2609+
!this.minimalMode &&
2610+
// We don't want to send a cache header for requests that contain dynamic
2611+
// data. If this is a Dynamic RSC request or wasn't a Prefetch RSC
2612+
// request, then we should set the cache header.
2613+
!isDynamicRSCRequest &&
2614+
(!didPostpone || isPrefetchRSCRequest)
2615+
) {
26052616
// set x-nextjs-cache header to match the header
26062617
// we set for the image-optimizer
26072618
res.setHeader(
@@ -2789,13 +2800,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
27892800
res.statusCode = cachedData.status
27902801
}
27912802

2792-
// Mark that the request did postpone if this is a data request or we're
2793-
// testing. It's used to verify that we're actually serving a postponed
2794-
// request so we can trust the cache headers.
2795-
if (
2796-
cachedData.postponed &&
2797-
(isRSCRequest || process.env.__NEXT_TEST_MODE)
2798-
) {
2803+
// Mark that the request did postpone if this is a data request.
2804+
if (cachedData.postponed && isRSCRequest) {
27992805
res.setHeader(NEXT_DID_POSTPONE_HEADER, '1')
28002806
}
28012807

@@ -2813,7 +2819,12 @@ export default abstract class Server<ServerOptions extends Options = Options> {
28132819
return {
28142820
type: 'rsc',
28152821
body: cachedData.html,
2816-
revalidate: cacheEntry.revalidate,
2822+
// Dynamic RSC responses cannot be cached, even if they're
2823+
// configured with `force-static` because we have no way of
2824+
// distinguishing between `force-static` and pages that have no
2825+
// postponed state.
2826+
// TODO: distinguish `force-static` from pages with no postponed state (static)
2827+
revalidate: 0,
28172828
}
28182829
}
28192830

@@ -2881,7 +2892,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
28812892
return {
28822893
type: 'html',
28832894
body,
2884-
revalidate: cacheEntry.revalidate,
2895+
// We don't want to cache the response if it has postponed data because
2896+
// the response being sent to the client it's dynamic parts are streamed
2897+
// to the client on the same request.
2898+
revalidate: 0,
28852899
}
28862900
} else if (isDataReq) {
28872901
return {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React, { Suspense } from 'react'
2+
import { Dynamic } from '../../../../../components/dynamic'
3+
4+
export const dynamic = 'force-dynamic'
5+
6+
export function generateStaticParams() {
7+
return []
8+
}
9+
10+
export default ({ params: { slug } }) => {
11+
return (
12+
<Suspense
13+
fallback={
14+
<Dynamic pathname={`/dynamic/force-dynamic/nested/${slug}`} fallback />
15+
}
16+
>
17+
<Dynamic pathname={`/dynamic/force-dynamic/nested/${slug}`} />
18+
</Suspense>
19+
)
20+
}

test/e2e/app-dir/ppr-full/app/dynamic/force-dynamic/page.jsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@ import { Dynamic } from '../../../components/dynamic'
33

44
export const dynamic = 'force-dynamic'
55

6-
export default ({ params: { slug } }) => {
6+
export default () => {
77
return (
8-
<Suspense
9-
fallback={
10-
<Dynamic pathname={`/dynamic/force-dynamic/${slug}`} fallback />
11-
}
12-
>
13-
<Dynamic pathname={`/dynamic/force-dynamic/${slug}`} />
8+
<Suspense fallback={<Dynamic pathname="/dynamic/force-dynamic" fallback />}>
9+
<Dynamic pathname="/dynamic/force-dynamic" />
1410
</Suspense>
1511
)
1612
}

test/e2e/app-dir/ppr-full/app/dynamic/force-static/page.jsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import { Dynamic } from '../../../components/dynamic'
44
export const dynamic = 'force-static'
55
export const revalidate = 60
66

7-
export default ({ params: { slug } }) => {
7+
export default () => {
88
return (
9-
<Suspense
10-
fallback={<Dynamic pathname={`/dynamic/force-static/${slug}`} fallback />}
11-
>
12-
<Dynamic pathname={`/dynamic/force-static/${slug}`} />
9+
<Suspense fallback={<Dynamic pathname="/dynamic/force-static" fallback />}>
10+
<Dynamic pathname="/dynamic/force-static" />
1311
</Suspense>
1412
)
1513
}

test/e2e/app-dir/ppr-full/app/layout.jsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
1-
import { Links } from '../components/links'
1+
import { Layout } from '../components/layout'
22

33
export default ({ children }) => {
44
return (
55
<html>
66
<body>
7-
<h1>Partial Prerendering</h1>
8-
<p>
9-
Below are links that are associated with different pages that all will
10-
partially prerender
11-
</p>
12-
<aside>
13-
<Links />
14-
</aside>
15-
<main>{children}</main>
7+
<Layout>{children}</Layout>
168
</body>
179
</html>
1810
)
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import React from 'react'
2-
import { Dynamic } from '../../components/dynamic'
3-
41
export default () => {
5-
return <Dynamic pathname="/static" fallback />
2+
return (
3+
<dl>
4+
<dt>Pathname</dt>
5+
<dd data-pathname="/static">/static</dd>
6+
</dl>
7+
)
68
}
Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
1-
import React from 'react'
2-
import { headers } from 'next/headers'
1+
import React, { use } from 'react'
2+
import * as next from 'next/headers'
33

44
export const Dynamic = ({ pathname, fallback }) => {
55
if (fallback) {
6-
return <div>Loading...</div>
6+
return <div>Dynamic Loading...</div>
77
}
88

9+
const headers = next.headers()
910
const messages = []
10-
const names = ['x-test-input', 'user-agent']
11-
const list = headers()
11+
for (const name of ['x-test-input', 'user-agent']) {
12+
messages.push({ name, value: headers.get(name) })
13+
}
1214

13-
for (const name of names) {
14-
messages.push({ name, value: list.get(name) })
15+
const delay = headers.get('x-delay')
16+
if (delay) {
17+
use(new Promise((resolve) => setTimeout(resolve, parseInt(delay, 10))))
1518
}
1619

1720
return (
18-
<div id="needle">
19-
<dl>
20-
{pathname && (
21-
<>
22-
<dt>Pathname</dt>
23-
<dd>{pathname}</dd>
24-
</>
25-
)}
26-
{messages.map(({ name, value }) => (
27-
<React.Fragment key={name}>
28-
<dt>
29-
Header: <code>{name}</code>
30-
</dt>
31-
<dd>{value ?? 'null'}</dd>
32-
</React.Fragment>
33-
))}
34-
</dl>
35-
</div>
21+
<dl>
22+
{pathname && (
23+
<>
24+
<dt>Pathname</dt>
25+
<dd data-pathname={pathname}>{pathname}</dd>
26+
</>
27+
)}
28+
{messages.map(({ name, value }) => (
29+
<React.Fragment key={name}>
30+
<dt>
31+
Header: <code>{name}</code>
32+
</dt>
33+
<dd>{value ?? `MISSING:${name.toUpperCase()}`}</dd>
34+
</React.Fragment>
35+
))}
36+
</dl>
3637
)
3738
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react'
2+
import { Links } from './links'
3+
4+
export const Layout = ({ children }) => {
5+
return (
6+
<>
7+
<h1>Partial Prerendering</h1>
8+
<p>
9+
Below are links that are associated with different pages that all will
10+
partially prerender
11+
</p>
12+
<aside>
13+
<Links />
14+
</aside>
15+
<main>{children}</main>
16+
</>
17+
)
18+
}

test/e2e/app-dir/ppr-full/components/links.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ const links = [
1818
{ href: '/no-suspense/nested/b', tag: 'no suspense, on-demand' },
1919
{ href: '/no-suspense/nested/c', tag: 'no suspense, on-demand' },
2020
{ href: '/dynamic/force-dynamic', tag: "dynamic = 'force-dynamic'" },
21+
{
22+
href: '/dynamic/force-dynamic/nested/a',
23+
tag: "dynamic = 'force-dynamic', on-demand, no-gsp",
24+
},
25+
{
26+
href: '/dynamic/force-dynamic/nested/b',
27+
tag: "dynamic = 'force-dynamic', on-demand, no-gsp",
28+
},
29+
{
30+
href: '/dynamic/force-dynamic/nested/c',
31+
tag: "dynamic = 'force-dynamic', on-demand, no-gsp",
32+
},
2133
{ href: '/dynamic/force-static', tag: "dynamic = 'force-static'" },
2234
{ href: '/edge/suspense', tag: 'edge, pre-generated' },
2335
{ href: '/edge/suspense/a', tag: 'edge, pre-generated' },
@@ -27,6 +39,7 @@ const links = [
2739
{ href: '/edge/no-suspense/a', tag: 'edge, no suspense, pre-generated' },
2840
{ href: '/edge/no-suspense/b', tag: 'edge, no suspense, on-demand' },
2941
{ href: '/edge/no-suspense/c', tag: 'edge, no suspense, on-demand' },
42+
{ href: '/pages', tag: 'pages' },
3043
]
3144

3245
export const Links = () => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
import { Layout } from '../components/layout'
3+
4+
export default () => {
5+
return (
6+
<Layout>
7+
<dl>
8+
<dt>Pathname</dt>
9+
<dd data-pathname="/pages">/pages</dd>
10+
</dl>
11+
</Layout>
12+
)
13+
}

0 commit comments

Comments
 (0)