Skip to content

Commit ede6277

Browse files
authoredFeb 16, 2024
fix: fetch cache tag invalidation (#268)
* fix: ensure fetch cache tags are checked against tag manifest * feat: use netlify-cache-tag instread of cache-tag * fix: return false from check stale * test: add test to catch subsequent revalidate by tags * test: change cache-tag assertions
1 parent 114e247 commit ede6277

File tree

6 files changed

+48
-12
lines changed

6 files changed

+48
-12
lines changed
 

‎src/run/handlers/cache.cts

+15-7
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export class NetlifyCacheHandler implements CacheHandler {
103103
return null
104104
}
105105

106-
const staleByTags = await this.checkCacheEntryStaleByTags(blob, ctx.softTags)
106+
const staleByTags = await this.checkCacheEntryStaleByTags(blob, ctx.tags, ctx.softTags)
107107

108108
if (staleByTags) {
109109
span.addEvent('Stale', { staleByTags })
@@ -197,13 +197,21 @@ export class NetlifyCacheHandler implements CacheHandler {
197197
/**
198198
* Checks if a page is stale through on demand revalidated tags
199199
*/
200-
private async checkCacheEntryStaleByTags(cacheEntry: CacheHandlerValue, softTags: string[] = []) {
201-
const tags =
202-
cacheEntry.value && 'headers' in cacheEntry.value
203-
? (cacheEntry.value.headers?.[NEXT_CACHE_TAGS_HEADER] as string)?.split(',') || []
204-
: []
200+
private async checkCacheEntryStaleByTags(
201+
cacheEntry: CacheHandlerValue,
202+
tags: string[] = [],
203+
softTags: string[] = [],
204+
) {
205+
let cacheTags: string[] = []
206+
207+
if (cacheEntry.value?.kind === 'FETCH') {
208+
cacheTags = [...tags, ...softTags]
209+
} else if (cacheEntry.value?.kind === 'PAGE' || cacheEntry.value?.kind === 'ROUTE') {
210+
cacheTags = (cacheEntry.value.headers?.[NEXT_CACHE_TAGS_HEADER] as string)?.split(',') || []
211+
} else {
212+
return false
213+
}
205214

206-
const cacheTags = [...tags, ...softTags]
207215
const allManifests = await Promise.all(
208216
cacheTags.map(async (tag) => {
209217
const res = await this.blobStore

‎src/run/headers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,9 @@ export const setCacheControlHeaders = (headers: Headers, request: Request) => {
202202
export const setCacheTagsHeaders = (headers: Headers, request: Request, manifest: TagsManifest) => {
203203
const path = new URL(request.url).pathname
204204
const tags = manifest[path]
205-
headers.set('cache-tag', tags)
205+
if (tags !== undefined) {
206+
headers.set('netlify-cache-tag', tags)
207+
}
206208
}
207209

208210
/**

‎tests/fixtures/server-components/app/static-fetch-1/page.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default async function Page() {
1313
<h1>Hello, Static Fetch 1</h1>
1414
<dl>
1515
<dt>Quote</dt>
16-
<dd>{data[0].quote}</dd>
16+
<dd data-testid="quote">{data[0].quote}</dd>
1717
<dt>Time</dt>
1818
<dd data-testid="date-now">{new Date().toISOString()}</dd>
1919
</dl>

‎tests/integration/page-router.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ test<FixtureTestContext>('Should add pathname to cache-tags for pages route', as
5959

6060
const staticFetch1 = await invokeFunction(ctx, { url: '/static/revalidate-manual' })
6161

62-
expect(staticFetch1.headers?.['cache-tag']).toBe('_N_T_/static/revalidate-manual')
62+
expect(staticFetch1.headers?.['netlify-cache-tag']).toBe('_N_T_/static/revalidate-manual')
6363
})
6464

6565
test<FixtureTestContext>('Should revalidate path with On-demand Revalidation', async (ctx) => {

‎tests/integration/revalidate-tags.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
3535
// test the function call
3636
const post1 = await invokeFunction(ctx, { url: '/static-fetch-1' })
3737
const post1Date = load(post1.body)('[data-testid="date-now"]').text()
38+
const post1Quote = load(post1.body)('[data-testid="quote"]').text()
3839
expect(post1.statusCode).toBe(200)
3940
expect(load(post1.body)('h1').text()).toBe('Hello, Static Fetch 1')
4041
expect(post1.headers, 'a cache hit on the first invocation of a prerendered page').toEqual(
@@ -53,6 +54,7 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
5354

5455
const post2 = await invokeFunction(ctx, { url: '/static-fetch-1' })
5556
const post2Date = load(post2.body)('[data-testid="date-now"]').text()
57+
const post2Quote = load(post2.body)('[data-testid="quote"]').text()
5658
expect(post2.statusCode).toBe(200)
5759
expect(load(post2.body)('h1').text()).toBe('Hello, Static Fetch 1')
5860
expect(post2.headers, 'a cache miss on the on demand revalidated page').toEqual(
@@ -62,12 +64,14 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
6264
}),
6365
)
6466
expect(post2Date).not.toBe(post1Date)
67+
expect(post2Quote).not.toBe(post1Quote)
6568

6669
// it does not wait for the cache.set so we have to manually wait here until the blob storage got populated
6770
await new Promise<void>((resolve) => setTimeout(resolve, 100))
6871

6972
const post3 = await invokeFunction(ctx, { url: '/static-fetch-1' })
7073
const post3Date = load(post3.body)('[data-testid="date-now"]').text()
74+
const post3Quote = load(post3.body)('[data-testid="quote"]').text()
7175
expect(post3.statusCode).toBe(200)
7276
expect(load(post3.body)('h1').text()).toBe('Hello, Static Fetch 1')
7377
expect(post3.headers, 'a cache hit on the revalidated and regenerated page').toEqual(
@@ -77,4 +81,26 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
7781
}),
7882
)
7983
expect(post3Date).toBe(post2Date)
84+
expect(post3Quote).toBe(post2Quote)
85+
86+
const revalidate2 = await invokeFunction(ctx, { url: '/api/on-demand-revalidate/tag' })
87+
expect(revalidate2.statusCode).toBe(200)
88+
expect(JSON.parse(revalidate2.body)).toEqual({ revalidated: true, now: expect.any(String) })
89+
90+
// it does not wait for the revalidation
91+
await new Promise<void>((resolve) => setTimeout(resolve, 100))
92+
93+
const post4 = await invokeFunction(ctx, { url: '/static-fetch-1' })
94+
const post4Date = load(post4.body)('[data-testid="date-now"]').text()
95+
const post4Quote = load(post4.body)('[data-testid="quote"]').text()
96+
expect(post4.statusCode).toBe(200)
97+
expect(load(post4.body)('h1').text()).toBe('Hello, Static Fetch 1')
98+
expect(post4.headers, 'a cache miss on the on demand revalidated page').toEqual(
99+
expect.objectContaining({
100+
'cache-status': '"Next.js"; fwd=miss',
101+
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
102+
}),
103+
)
104+
expect(post4Date).not.toBe(post3Date)
105+
expect(post4Quote).not.toBe(post3Quote)
80106
})

‎tests/integration/simple-app.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ test<FixtureTestContext>('Should add cache-tags to prerendered app pages', async
6666

6767
const staticFetch1 = await invokeFunction(ctx, { url: '/other' })
6868

69-
expect(staticFetch1.headers?.['cache-tag']).toBe(
69+
expect(staticFetch1.headers?.['netlify-cache-tag']).toBe(
7070
'_N_T_/layout,_N_T_/other/layout,_N_T_/other/page,_N_T_/other',
7171
)
7272
})
@@ -76,7 +76,7 @@ test<FixtureTestContext>('index should be normalized within the cacheHandler and
7676
await runPlugin(ctx)
7777
const index = await invokeFunction(ctx, { url: '/' })
7878
expect(index.statusCode).toBe(200)
79-
expect(index.headers?.['cache-tag']).toBe('_N_T_/layout,_N_T_/page,_N_T_/')
79+
expect(index.headers?.['netlify-cache-tag']).toBe('_N_T_/layout,_N_T_/page,_N_T_/')
8080
})
8181

8282
test<FixtureTestContext>('stale-while-revalidate headers should be normalized to include delta-seconds', async (ctx) => {

0 commit comments

Comments
 (0)
Please sign in to comment.