Skip to content

Commit 092df0f

Browse files
committed
support experimental.validateRSCRequestHeaders for runtime prefetches
1 parent 63e373b commit 092df0f

File tree

4 files changed

+240
-107
lines changed

4 files changed

+240
-107
lines changed

test/e2e/app-dir/segment-cache/cdn-cache-busting/app/page.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,37 @@ import { LinkAccordion } from '../components/link-accordion'
22

33
export default function Page() {
44
return (
5-
<ul>
6-
<li>
7-
<LinkAccordion href="/target-page">Target page</LinkAccordion>
8-
</li>
9-
<li>
10-
<LinkAccordion href="/redirect-to-target-page">
11-
Redirects to target page
12-
</LinkAccordion>
13-
</li>
14-
</ul>
5+
<div>
6+
<h2>auto prefetches</h2>
7+
<ul id="prefetch-auto">
8+
<li>
9+
<LinkAccordion href="/target-page">Target page</LinkAccordion>
10+
</li>
11+
<li>
12+
<LinkAccordion href="/redirect-to-target-page">
13+
Redirects to target page
14+
</LinkAccordion>
15+
</li>
16+
</ul>
17+
18+
{process.env.__NEXT_CACHE_COMPONENTS && (
19+
// runtime prefetches are only available if cacheComponents is enabled.
20+
<>
21+
<h2>runtime prefetches</h2>
22+
<ul id="prefetch-runtime">
23+
<li>
24+
<LinkAccordion href="/target-page" prefetch={true}>
25+
Target page
26+
</LinkAccordion>
27+
</li>
28+
<li>
29+
<LinkAccordion href="/redirect-to-target-page" prefetch={true}>
30+
Redirects to target page
31+
</LinkAccordion>
32+
</li>
33+
</ul>
34+
</>
35+
)}
36+
</div>
1537
)
1638
}

test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts

Lines changed: 192 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import { findPort, nextBuild } from 'next-test-utils'
55
import { isNextDeploy, isNextDev } from 'e2e-utils'
66
import { start } from './server.mjs'
77

8+
const isCacheComponentsEnabled =
9+
process.env.__NEXT_EXPERIMENTAL_CACHE_COMPONENTS === 'true'
10+
811
describe('segment cache (CDN cache busting)', () => {
912
if (isNextDev || isNextDeploy) {
1013
test('should not run during dev or deploy test runs', () => {})
1114
return
1215
}
1316

14-
// TODO(runtime-ppr): add tests for runtime prefetches
15-
1617
// To debug these tests locally, run:
1718
// node start.mjs
1819
//
@@ -34,110 +35,216 @@ describe('segment cache (CDN cache busting)', () => {
3435
await cleanup()
3536
})
3637

37-
it(
38+
describe(
3839
"perform fully prefetched navigation with a CDN that doesn't respect " +
3940
'the Vary header',
40-
async () => {
41-
let act
42-
const browser = await webdriver(port, '/', {
43-
beforePageLoad(p: Playwright.Page) {
44-
act = createRouterAct(p)
45-
},
41+
() => {
42+
it('static prefetch', async () => {
43+
let act
44+
const browser = await webdriver(port, '/', {
45+
beforePageLoad(p: Playwright.Page) {
46+
act = createRouterAct(p)
47+
},
48+
})
49+
50+
// Initiate a prefetch. Each segment will be prefetched individually,
51+
// using the pathname of the target page and a custom header specifying
52+
// the segment. If we didn't also set a cache-busting search param, then
53+
// the fake CDN used by this test suite would incorrectly use the same
54+
// entry for every segment, poisoning the cache.
55+
await act(
56+
async () => {
57+
const linkToggle = await browser.elementByCss(
58+
'#prefetch-auto [data-link-accordion="/target-page"]'
59+
)
60+
await linkToggle.click()
61+
},
62+
{
63+
includes: 'Target page',
64+
}
65+
)
66+
67+
// Navigate to the prefetched target page.
68+
await act(async () => {
69+
const link = await browser.elementByCss('a[href="/target-page"]')
70+
await link.click()
71+
72+
// The page was prefetched, so we're able to render the target
73+
// page immediately.
74+
const div = await browser.elementById('target-page')
75+
expect(await div.text()).toBe('Target page')
76+
}, 'no-requests')
4677
})
4778

48-
// Initiate a prefetch. Each segment will be prefetched individually,
49-
// using the pathname of the target page and a custom header specifying
50-
// the segment. If we didn't also set a cache-busting search param, then
51-
// the fake CDN used by this test suite would incorrectly use the same
52-
// entry for every segment, poisoning the cache.
53-
await act(
54-
async () => {
55-
const linkToggle = await browser.elementByCss(
56-
'[data-link-accordion="/target-page"]'
79+
if (isCacheComponentsEnabled) {
80+
it('runtime prefetch', async () => {
81+
let act
82+
const browser = await webdriver(port, '/', {
83+
beforePageLoad(p: Playwright.Page) {
84+
act = createRouterAct(p)
85+
},
86+
})
87+
88+
// Initiate a prefetch. We'll send two requests - one to prefetch the tree,
89+
// and another to prefetch the page content (which is static, so it will be complete).
90+
// If we didn't also set a cache-busting search param, then
91+
// the fake CDN used by this test suite would incorrectly use the same
92+
// entry for both responses, poisoning the cache.
93+
await act(
94+
async () => {
95+
const linkToggle = await browser.elementByCss(
96+
'#prefetch-runtime [data-link-accordion="/target-page"]'
97+
)
98+
await linkToggle.click()
99+
},
100+
{
101+
// This should be returned as part of the second request, if it wasn't cache poisoned.
102+
includes: 'Target page',
103+
}
57104
)
58-
await linkToggle.click()
59-
},
60-
{
61-
includes: 'Target page',
62-
}
63-
)
64-
65-
// Navigate to the prefetched target page.
66-
await act(async () => {
67-
const link = await browser.elementByCss('a[href="/target-page"]')
68-
await link.click()
69-
70-
// The page was prefetched, so we're able to render the target
71-
// page immediately.
72-
const div = await browser.elementById('target-page')
73-
expect(await div.text()).toBe('Target page')
74-
}, 'no-requests')
105+
106+
// Navigate to the prefetched target page.
107+
await act(async () => {
108+
const link = await browser.elementByCss('a[href="/target-page"]')
109+
await link.click()
110+
111+
// The page was prefetched, so we're able to render the target
112+
// page immediately.
113+
const div = await browser.elementById('target-page')
114+
expect(await div.text()).toBe('Target page')
115+
}, 'no-requests')
116+
})
117+
}
75118
}
76119
)
77120

78-
it(
121+
describe(
79122
'prevent cache poisoning attacks by responding with a redirect to correct ' +
80123
'cache busting query param if a custom header is sent during a prefetch ' +
81124
'without a corresponding cache-busting search param',
82-
async () => {
83-
const browser = await webdriver(port, '/')
84-
const { status, responseUrl, redirected } = await browser.eval(
85-
async () => {
86-
const res = await fetch('/target-page', {
87-
headers: {
88-
rsc: '1',
89-
'next-router-prefetch': '1',
90-
'next-router-segment-prefetch': '/_tree',
91-
},
92-
})
93-
return {
94-
status: res.status,
95-
responseUrl: res.url,
96-
redirected: res.redirected,
125+
() => {
126+
it('static prefetch', async () => {
127+
const browser = await webdriver(port, '/')
128+
const { status, responseUrl, redirected } = await browser.eval(
129+
async () => {
130+
const res = await fetch('/target-page', {
131+
headers: {
132+
rsc: '1',
133+
'next-router-prefetch': '1',
134+
'next-router-segment-prefetch': '/_tree',
135+
},
136+
})
137+
return {
138+
status: res.status,
139+
responseUrl: res.url,
140+
redirected: res.redirected,
141+
}
97142
}
98-
}
99-
)
100-
expect(status).toBe(200)
101-
expect(responseUrl).toContain('_rsc=')
102-
expect(redirected).toBe(true)
143+
)
144+
expect(status).toBe(200)
145+
expect(responseUrl).toContain('_rsc=')
146+
expect(redirected).toBe(true)
147+
})
148+
149+
if (isCacheComponentsEnabled) {
150+
it('runtime prefetch', async () => {
151+
const browser = await webdriver(port, '/')
152+
const { status, responseUrl, redirected } = await browser.eval(
153+
async () => {
154+
const res = await fetch('/target-page', {
155+
headers: {
156+
rsc: '1',
157+
'next-router-prefetch': '2',
158+
},
159+
})
160+
return {
161+
status: res.status,
162+
responseUrl: res.url,
163+
redirected: res.redirected,
164+
}
165+
}
166+
)
167+
expect(status).toBe(200)
168+
expect(responseUrl).toContain('_rsc=')
169+
expect(redirected).toBe(true)
170+
})
171+
}
103172
}
104173
)
105174

106-
it(
175+
describe(
107176
'perform fully prefetched navigation when a third-party proxy ' +
108177
'performs a redirect',
109-
async () => {
110-
let act
111-
const browser = await webdriver(port, '/', {
112-
beforePageLoad(p: Playwright.Page) {
113-
act = createRouterAct(p)
114-
},
178+
() => {
179+
it('static prefetch', async () => {
180+
let act
181+
const browser = await webdriver(port, '/', {
182+
beforePageLoad(p: Playwright.Page) {
183+
act = createRouterAct(p)
184+
},
185+
})
186+
187+
await act(
188+
async () => {
189+
const linkToggle = await browser.elementByCss(
190+
'#prefetch-auto [data-link-accordion="/redirect-to-target-page"]'
191+
)
192+
await linkToggle.click()
193+
},
194+
{
195+
includes: 'Target page',
196+
}
197+
)
198+
199+
// Navigate to the prefetched target page.
200+
await act(async () => {
201+
const link = await browser.elementByCss(
202+
'a[href="/redirect-to-target-page"]'
203+
)
204+
await link.click()
205+
206+
// The page was prefetched, so we're able to render the target
207+
// page immediately.
208+
const div = await browser.elementById('target-page')
209+
expect(await div.text()).toBe('Target page')
210+
}, 'no-requests')
115211
})
116212

117-
await act(
118-
async () => {
119-
const linkToggle = await browser.elementByCss(
120-
'[data-link-accordion="/redirect-to-target-page"]'
213+
if (isCacheComponentsEnabled) {
214+
it('runtime prefetch', async () => {
215+
let act
216+
const browser = await webdriver(port, '/', {
217+
beforePageLoad(p: Playwright.Page) {
218+
act = createRouterAct(p)
219+
},
220+
})
221+
222+
await act(
223+
async () => {
224+
const linkToggle = await browser.elementByCss(
225+
'#prefetch-runtime [data-link-accordion="/redirect-to-target-page"]'
226+
)
227+
await linkToggle.click()
228+
},
229+
{
230+
includes: 'Target page',
231+
}
121232
)
122-
await linkToggle.click()
123-
},
124-
{
125-
includes: 'Target page',
126-
}
127-
)
128-
129-
// Navigate to the prefetched target page.
130-
await act(async () => {
131-
const link = await browser.elementByCss(
132-
'a[href="/redirect-to-target-page"]'
133-
)
134-
await link.click()
135233

136-
// The page was prefetched, so we're able to render the target
137-
// page immediately.
138-
const div = await browser.elementById('target-page')
139-
expect(await div.text()).toBe('Target page')
140-
}, 'no-requests')
234+
// Navigate to the prefetched target page.
235+
await act(async () => {
236+
const link = await browser.elementByCss(
237+
'a[href="/redirect-to-target-page"]'
238+
)
239+
await link.click()
240+
241+
// The page was prefetched, so we're able to render the target
242+
// page immediately.
243+
const div = await browser.elementById('target-page')
244+
expect(await div.text()).toBe('Target page')
245+
}, 'no-requests')
246+
})
247+
}
141248
}
142249
)
143250
})

test/e2e/app-dir/segment-cache/cdn-cache-busting/server.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,15 @@ async function createFakeCDN(destPort) {
135135
}
136136
resolveCDNEntry(entry)
137137
})
138-
return
138+
} else {
139+
// If the response isn't cacheable, pipe it through to the client.
140+
res.writeHead(
141+
proxyRes.statusCode || 200,
142+
proxyRes.statusMessage,
143+
proxyRes.headers
144+
)
145+
proxyRes.pipe(res)
139146
}
140-
// If the response isn't cacheable, pipe it through to the client.
141-
proxyRes.pipe(res)
142-
return
143147
})
144148

145149
return cdnServer

0 commit comments

Comments
 (0)