Skip to content

Commit d85332a

Browse files
piehorinokai
andauthored
fix: make on-demand revalidation reliable (#245)
* test: update assertions in e2e * test(e2e): add test cases for on-demand revalidation for app router * fix: use strong consistency for CacheHandler --------- Co-authored-by: Rob Stanford <[email protected]>
1 parent 4a0c285 commit d85332a

File tree

20 files changed

+576
-76
lines changed

20 files changed

+576
-76
lines changed

src/run/handlers/cache.cts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class NetlifyCacheHandler implements CacheHandler {
2727
constructor(options: CacheHandlerContext) {
2828
this.options = options
2929
this.revalidatedTags = options.revalidatedTags
30-
this.blobStore = getDeployStore({ fetch: fetchBeforeNextPatchedIt })
30+
this.blobStore = getDeployStore({ fetch: fetchBeforeNextPatchedIt, consistency: 'strong' })
3131
}
3232

3333
private async encodeBlobKey(key: string) {

src/run/headers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export const adjustDateHeader = async (
103103
}
104104
const key = new URL(request.url).pathname
105105
const blobKey = await encodeBlobKey(key)
106-
const blobStore = getDeployStore({ fetch: fetchBeforeNextPatchedIt })
106+
const blobStore = getDeployStore({ fetch: fetchBeforeNextPatchedIt, consistency: 'strong' })
107107

108108
// TODO: use metadata for this
109109
const { lastModified } = await tracer.startActiveSpan(

tests/e2e/on-demand-app.test.ts

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { expect } from '@playwright/test'
2+
import { test } from '../utils/create-e2e-fixture.js'
3+
4+
test.describe('app router on-demand revalidation', () => {
5+
test('revalidatePath', async ({ page, serverComponents }) => {
6+
// in case there is retry or some other test did hit that path before
7+
// we want to make sure that cdn cache is not warmed up
8+
const purgeCdnCache = await page.goto(
9+
new URL('/api/purge-cdn?path=/static-fetch/1', serverComponents.url).href,
10+
)
11+
expect(purgeCdnCache?.status()).toBe(200)
12+
13+
const response1 = await page.goto(new URL('static-fetch/1', serverComponents.url).href)
14+
const headers1 = response1?.headers() || {}
15+
expect(response1?.status()).toBe(200)
16+
expect(headers1['x-nextjs-cache']).toBeUndefined()
17+
// either first time hitting this route or we invalidated
18+
// just CDN node in earlier step
19+
// we will invoke function and see Next cache hit status \
20+
// in the response because it was prerendered at build time
21+
// or regenerated in previous attempt to run this test
22+
expect(headers1['cache-status']).toMatch(/"Netlify Edge"; fwd=(miss|stale)/m)
23+
expect(headers1['cache-status']).toMatch(/"Next.js"; hit/m)
24+
expect(headers1['netlify-cdn-cache-control']).toBe(
25+
's-maxage=31536000, stale-while-revalidate=31536000',
26+
)
27+
28+
const date1 = await page.textContent('[data-testid="date-now"]')
29+
30+
const h1 = await page.textContent('h1')
31+
expect(h1).toBe('Hello, Statically fetched show 1')
32+
33+
const response2 = await page.goto(new URL('static-fetch/1', serverComponents.url).href)
34+
const headers2 = response2?.headers() || {}
35+
expect(response2?.status()).toBe(200)
36+
expect(headers2['x-nextjs-cache']).toBeUndefined()
37+
// we are hitting the same page again and we most likely will see
38+
// CDN hit (in this case Next reported cache status is omitted
39+
// as it didn't actually take place in handling this request)
40+
// or we will see CDN miss because different CDN node handled request
41+
expect(headers2['cache-status']).toMatch(/"Netlify Edge"; (hit|fwd=miss|fwd=stale)/m)
42+
if (!headers2['cache-status'].includes('hit')) {
43+
// if we missed CDN cache, we will see Next cache hit status
44+
// as we reuse cached response
45+
expect(headers2['cache-status']).toMatch(/"Next.js"; hit/m)
46+
}
47+
expect(headers2['netlify-cdn-cache-control']).toBe(
48+
's-maxage=31536000, stale-while-revalidate=31536000',
49+
)
50+
51+
// the page is cached
52+
const date2 = await page.textContent('[data-testid="date-now"]')
53+
expect(date2).toBe(date1)
54+
55+
const revalidate = await page.goto(
56+
new URL('/api/on-demand-revalidate/path', serverComponents.url).href,
57+
)
58+
expect(revalidate?.status()).toBe(200)
59+
60+
// now after the revalidation it should have a different date
61+
const response3 = await page.goto(new URL('static-fetch/1', serverComponents.url).href)
62+
const headers3 = response3?.headers() || {}
63+
expect(response3?.status()).toBe(200)
64+
expect(headers3?.['x-nextjs-cache']).toBeUndefined()
65+
// revalidatePath just marks the page(s) as invalid and does NOT
66+
// automatically refreshes the cache. This request will cause
67+
// Next.js cache miss and new response will be generated and cached
68+
// Depending if we hit same CDN node as previous request, we might
69+
// get either fwd=miss or fwd=stale
70+
expect(headers3['cache-status']).toMatch(/"Next.js"; fwd=miss/m)
71+
expect(headers3['cache-status']).toMatch(/"Netlify Edge"; fwd=(miss|stale)/m)
72+
expect(headers3['netlify-cdn-cache-control']).toBe(
73+
's-maxage=31536000, stale-while-revalidate=31536000',
74+
)
75+
76+
// the page has now an updated date
77+
const date3 = await page.textContent('[data-testid="date-now"]')
78+
expect(date3).not.toBe(date2)
79+
80+
const response4 = await page.goto(new URL('static-fetch/1', serverComponents.url).href)
81+
const headers4 = response4?.headers() || {}
82+
expect(response4?.status()).toBe(200)
83+
expect(headers4?.['x-nextjs-cache']).toBeUndefined()
84+
// we are hitting the same page again and we most likely will see
85+
// CDN hit (in this case Next reported cache status is omitted
86+
// as it didn't actually take place in handling this request)
87+
// or we will see CDN miss because different CDN node handled request
88+
expect(headers4['cache-status']).toMatch(/"Netlify Edge"; (hit|fwd=miss|fwd=stale)/m)
89+
if (!headers4['cache-status'].includes('hit')) {
90+
// if we missed CDN cache, we will see Next cache hit status
91+
// as we reuse cached response
92+
expect(headers4['cache-status']).toMatch(/"Next.js"; hit/m)
93+
}
94+
expect(headers4['netlify-cdn-cache-control']).toBe(
95+
's-maxage=31536000, stale-while-revalidate=31536000',
96+
)
97+
98+
// the page is cached
99+
const date4 = await page.textContent('[data-testid="date-now"]')
100+
expect(date4).toBe(date3)
101+
})
102+
103+
test('revalidateTag', async ({ page, serverComponents }) => {
104+
// in case there is retry or some other test did hit that path before
105+
// we want to make sure that cdn cache is not warmed up
106+
const purgeCdnCache = await page.goto(
107+
new URL('/api/purge-cdn?path=/static-fetch-1', serverComponents.url).href,
108+
)
109+
expect(purgeCdnCache?.status()).toBe(200)
110+
111+
const response1 = await page.goto(new URL('static-fetch-1', serverComponents.url).href)
112+
const headers1 = response1?.headers() || {}
113+
expect(response1?.status()).toBe(200)
114+
expect(headers1['x-nextjs-cache']).toBeUndefined()
115+
// either first time hitting this route or we invalidated
116+
// just CDN node in earlier step
117+
// we will invoke function and see Next cache hit status \
118+
// in the response because it was prerendered at build time
119+
// or regenerated in previous attempt to run this test
120+
expect(headers1['cache-status']).toMatch(/"Netlify Edge"; fwd=(miss|stale)/m)
121+
expect(headers1['cache-status']).toMatch(/"Next.js"; hit/m)
122+
expect(headers1['netlify-cdn-cache-control']).toBe(
123+
's-maxage=31536000, stale-while-revalidate=31536000',
124+
)
125+
126+
const date1 = await page.textContent('[data-testid="date-now"]')
127+
128+
const h1 = await page.textContent('h1')
129+
expect(h1).toBe('Hello, Static Fetch 1')
130+
131+
const response2 = await page.goto(new URL('static-fetch-1', serverComponents.url).href)
132+
const headers2 = response2?.headers() || {}
133+
expect(response2?.status()).toBe(200)
134+
expect(headers2['x-nextjs-cache']).toBeUndefined()
135+
// we are hitting the same page again and we most likely will see
136+
// CDN hit (in this case Next reported cache status is omitted
137+
// as it didn't actually take place in handling this request)
138+
// or we will see CDN miss because different CDN node handled request
139+
expect(headers2['cache-status']).toMatch(/"Netlify Edge"; (hit|fwd=miss|fwd=stale)/m)
140+
if (!headers2['cache-status'].includes('hit')) {
141+
// if we missed CDN cache, we will see Next cache hit status
142+
// as we reuse cached response
143+
expect(headers2['cache-status']).toMatch(/"Next.js"; hit/m)
144+
}
145+
expect(headers2['netlify-cdn-cache-control']).toBe(
146+
's-maxage=31536000, stale-while-revalidate=31536000',
147+
)
148+
149+
// the page is cached
150+
const date2 = await page.textContent('[data-testid="date-now"]')
151+
expect(date2).toBe(date1)
152+
153+
const revalidate = await page.goto(
154+
new URL('/api/on-demand-revalidate/tag', serverComponents.url).href,
155+
)
156+
expect(revalidate?.status()).toBe(200)
157+
158+
// now after the revalidation it should have a different date
159+
const response3 = await page.goto(new URL('static-fetch-1', serverComponents.url).href)
160+
const headers3 = response3?.headers() || {}
161+
expect(response3?.status()).toBe(200)
162+
expect(headers3?.['x-nextjs-cache']).toBeUndefined()
163+
// revalidateTag just marks the page(s) as invalid and does NOT
164+
// automatically refreshes the cache. This request will cause
165+
// Next.js cache miss and new response will be generated and cached
166+
// Depending if we hit same CDN node as previous request, we might
167+
// get either fwd=miss or fwd=stale
168+
expect(headers3['cache-status']).toMatch(/"Next.js"; fwd=miss/m)
169+
expect(headers3['cache-status']).toMatch(/"Netlify Edge"; fwd=(miss|stale)/m)
170+
expect(headers3['netlify-cdn-cache-control']).toBe(
171+
's-maxage=31536000, stale-while-revalidate=31536000',
172+
)
173+
174+
// the page has now an updated date
175+
const date3 = await page.textContent('[data-testid="date-now"]')
176+
expect(date3).not.toBe(date2)
177+
178+
const response4 = await page.goto(new URL('static-fetch-1', serverComponents.url).href)
179+
const headers4 = response4?.headers() || {}
180+
expect(response4?.status()).toBe(200)
181+
expect(headers4?.['x-nextjs-cache']).toBeUndefined()
182+
// we are hitting the same page again and we most likely will see
183+
// CDN hit (in this case Next reported cache status is omitted
184+
// as it didn't actually take place in handling this request)
185+
// or we will see CDN miss because different CDN node handled request
186+
expect(headers4['cache-status']).toMatch(/"Netlify Edge"; (hit|fwd=miss|fwd=stale)/m)
187+
if (!headers4['cache-status'].includes('hit')) {
188+
// if we missed CDN cache, we will see Next cache hit status
189+
// as we reuse cached response
190+
expect(headers4['cache-status']).toMatch(/"Next.js"; hit/m)
191+
}
192+
expect(headers4['netlify-cdn-cache-control']).toBe(
193+
's-maxage=31536000, stale-while-revalidate=31536000',
194+
)
195+
196+
// the page is cached
197+
const date4 = await page.textContent('[data-testid="date-now"]')
198+
expect(date4).toBe(date3)
199+
})
200+
})

tests/e2e/page-router.test.ts

+30-24
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,27 @@ import { expect } from '@playwright/test'
22
import { test } from '../utils/create-e2e-fixture.js'
33

44
test('Static revalidate works correctly', async ({ page, pageRouter }) => {
5-
// this disables browser cache - otherwise If-None-Match header
6-
// is added to repeat requests which result in actual 304 response
7-
// with custom headers from function not being returned
8-
// additionally Playwright wrongly report 304 as 200
9-
// https://github.com/microsoft/playwright/issues/27573
10-
// which makes the assertions confusing
11-
// see https://playwright.dev/docs/api/class-browsercontext#browser-context-route
12-
// > Enabling routing disables http cache.
13-
// and https://stackoverflow.com/questions/68522170/playwright-disable-caching-of-webpage-so-i-can-fetch-new-elements-after-scrollin
14-
// note - this is likely the same problem that cause assertions at the bottom to be commented out
15-
// generally we shouldn't do that
16-
page.route('**', (route) => route.continue())
5+
// in case there is retry or some other test did hit that path before
6+
// we want to make sure that cdn cache is not warmed up
7+
const purgeCdnCache = await page.goto(
8+
new URL('/api/purge-cdn?path=/static/revalidate-manual', pageRouter.url).href,
9+
)
10+
expect(purgeCdnCache?.status()).toBe(200)
1711

1812
const response1 = await page.goto(new URL('static/revalidate-manual', pageRouter.url).href)
1913
const headers1 = response1?.headers() || {}
2014
expect(response1?.status()).toBe(200)
2115
expect(headers1['x-nextjs-cache']).toBeUndefined()
22-
// first time hitting this route - we will invoke function and see
23-
// Next cache hit status in the response because it was prerendered
24-
// at build time
25-
expect(headers1['cache-status']).toMatch(/"Netlify Edge"; fwd=miss/m)
16+
// either first time hitting this route or we invalidated
17+
// just CDN node in earlier step
18+
// we will invoke function and see Next cache hit status \
19+
// in the response because it was prerendered at build time
20+
// or regenerated in previous attempt to run this test
21+
expect(headers1['cache-status']).toMatch(/"Netlify Edge"; fwd=(miss|stale)/m)
2622
expect(headers1['cache-status']).toMatch(/"Next.js"; hit/m)
23+
expect(headers1['netlify-cdn-cache-control']).toBe(
24+
's-maxage=31536000, stale-while-revalidate=31536000',
25+
)
2726

2827
const date1 = await page.textContent('[data-testid="date-now"]')
2928
const h1 = await page.textContent('h1')
@@ -33,10 +32,19 @@ test('Static revalidate works correctly', async ({ page, pageRouter }) => {
3332
const headers2 = response2?.headers() || {}
3433
expect(response2?.status()).toBe(200)
3534
expect(headers2['x-nextjs-cache']).toBeUndefined()
36-
// On CDN hit, Next cache status is not added to response anymore
37-
// (any cache-status set by functions is not added - this is a platform behavior
38-
// not runtime behavior)
39-
expect(headers2['cache-status']).toMatch(/"Netlify Edge"; hit/m)
35+
// we are hitting the same page again and we most likely will see
36+
// CDN hit (in this case Next reported cache status is omitted
37+
// as it didn't actually take place in handling this request)
38+
// or we will see CDN miss because different CDN node handled request
39+
expect(headers2['cache-status']).toMatch(/"Netlify Edge"; (hit|fwd=miss|fwd=stale)/m)
40+
if (!headers2['cache-status'].includes('hit')) {
41+
// if we missed CDN cache, we will see Next cache hit status
42+
// as we reuse cached response
43+
expect(headers2['cache-status']).toMatch(/"Next.js"; hit/m)
44+
}
45+
expect(headers2['netlify-cdn-cache-control']).toBe(
46+
's-maxage=31536000, stale-while-revalidate=31536000',
47+
)
4048

4149
// the page is cached
4250
const date2 = await page.textContent('[data-testid="date-now"]')
@@ -62,10 +70,8 @@ test('Static revalidate works correctly', async ({ page, pageRouter }) => {
6270
expect(headers3['cache-status']).toMatch(/"Netlify Edge"; fwd=stale/m)
6371

6472
// the page has now an updated date
65-
// TODO: Cache purge is currently not working as expected
66-
// @Rob is working on it
67-
// const date3 = await page.textContent('[data-testid="date-now"]')
68-
// expect(date3).not.toBe(date2)
73+
const date3 = await page.textContent('[data-testid="date-now"]')
74+
expect(date3).not.toBe(date2)
6975
})
7076

7177
test('requesting a non existing page route that needs to be fetched from the blob store like 404.html', async ({

0 commit comments

Comments
 (0)