Skip to content

Commit 39a1c2a

Browse files
fix(fetch-cache): add check for updated tags when checking same cache key (#63547)
## Why? When we fetch the same cache key (URL) but add an additional tag, the revalidation does not re-fetch correctly (the bug just uses in-memory cache again) when deployed. :repro: → https://github.com/lostip/nextjs-revalidation-demo/tree/main --------- Co-authored-by: Ethan Arrowood <ethan@arrowood.dev>
1 parent 8c9c1d2 commit 39a1c2a

File tree

4 files changed

+105
-2
lines changed

4 files changed

+105
-2
lines changed

packages/next/src/server/lib/incremental-cache/fetch-cache.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ export default class FetchCache implements CacheHandler {
2828
private cacheEndpoint?: string
2929
private debug: boolean
3030

31+
private hasMatchingTags(arr1: string[], arr2: string[]) {
32+
if (arr1.length !== arr2.length) return false
33+
34+
const set1 = new Set(arr1)
35+
const set2 = new Set(arr2)
36+
37+
if (set1.size !== set2.size) return false
38+
39+
for (let tag of set1) {
40+
if (!set2.has(tag)) return false
41+
}
42+
43+
return true
44+
}
45+
3146
static isAvailable(ctx: {
3247
_requestHeaders: CacheHandlerContext['_requestHeaders']
3348
}) {
@@ -168,8 +183,13 @@ export default class FetchCache implements CacheHandler {
168183
// on successive requests
169184
let data = memoryCache?.get(key)
170185

171-
// get data from fetch cache
172-
if (!data && this.cacheEndpoint) {
186+
const hasFetchKindAndMatchingTags =
187+
data?.value?.kind === 'FETCH' &&
188+
this.hasMatchingTags(tags ?? [], data.value.tags ?? [])
189+
190+
// Get data from fetch cache. Also check if new tags have been
191+
// specified with the same cache key (fetch URL)
192+
if (this.cacheEndpoint && (!data || !hasFetchKindAndMatchingTags)) {
173193
try {
174194
const start = Date.now()
175195
const fetchParams: NextFetchCacheParams = {
@@ -220,6 +240,16 @@ export default class FetchCache implements CacheHandler {
220240
throw new Error(`invalid cache value`)
221241
}
222242

243+
// if new tags were specified, merge those tags to the existing tags
244+
if (cached.kind === 'FETCH') {
245+
cached.tags ??= []
246+
for (const tag of tags ?? []) {
247+
if (!cached.tags.include(tag)) {
248+
cached.tag.push(tag)
249+
}
250+
}
251+
}
252+
223253
const cacheState = res.headers.get(CACHE_STATE_HEADER)
224254
const age = res.headers.get('age')
225255

test/e2e/app-dir/app-static/app-static.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,49 @@ createNextDescribe(
4949
})
5050
})
5151

52+
if (isNextDeploy) {
53+
describe('new tags have been specified on subsequent fetch', () => {
54+
it('should not fetch from memory cache', async () => {
55+
const res1 = await next.fetch('/specify-new-tags/one-tag')
56+
expect(res1.status).toBe(200)
57+
58+
const res2 = await next.fetch('/specify-new-tags/two-tags')
59+
expect(res2.status).toBe(200)
60+
61+
const html1 = await res1.text()
62+
const html2 = await res2.text()
63+
const $1 = cheerio.load(html1)
64+
const $2 = cheerio.load(html2)
65+
66+
const data1 = $1('#page-data').text()
67+
const data2 = $2('#page-data').text()
68+
expect(data1).not.toBe(data2)
69+
})
70+
71+
it('should not fetch from memory cache after revalidateTag is used', async () => {
72+
const res1 = await next.fetch('/specify-new-tags/one-tag')
73+
expect(res1.status).toBe(200)
74+
75+
const revalidateRes = await next.fetch(
76+
'/api/revlidate-tag-node?tag=thankyounext'
77+
)
78+
expect((await revalidateRes.json()).revalidated).toBe(true)
79+
80+
const res2 = await next.fetch('/specify-new-tags/two-tags')
81+
expect(res2.status).toBe(200)
82+
83+
const html1 = await res1.text()
84+
const html2 = await res2.text()
85+
const $1 = cheerio.load(html1)
86+
const $2 = cheerio.load(html2)
87+
88+
const data1 = $1('#page-data').text()
89+
const data2 = $2('#page-data').text()
90+
expect(data1).not.toBe(data2)
91+
})
92+
})
93+
}
94+
5295
if (isNextStart) {
5396
it('should propagate unstable_cache tags correctly', async () => {
5497
const meta = JSON.parse(
@@ -717,6 +760,10 @@ createNextDescribe(
717760
"route-handler/revalidate-360-isr/route.js",
718761
"route-handler/revalidate-360/route.js",
719762
"route-handler/static-cookies/route.js",
763+
"specify-new-tags/one-tag/page.js",
764+
"specify-new-tags/one-tag/page_client-reference-manifest.js",
765+
"specify-new-tags/two-tags/page.js",
766+
"specify-new-tags/two-tags/page_client-reference-manifest.js",
720767
"ssg-draft-mode.html",
721768
"ssg-draft-mode.rsc",
722769
"ssg-draft-mode/[[...route]]/page.js",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const dynamic = 'force-dynamic'
2+
3+
export default async function Page() {
4+
const data = await fetch(
5+
'https://next-data-api-endpoint.vercel.app/api/random?sam=iam',
6+
{
7+
cache: 'force-cache',
8+
next: { tags: ['thankyounext'] },
9+
}
10+
).then((res) => res.text())
11+
12+
return <p id="page-data">data: {data}</p>
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const dynamic = 'force-dynamic'
2+
3+
export default async function Page() {
4+
const data = await fetch(
5+
'https://next-data-api-endpoint.vercel.app/api/random?sam=iam',
6+
{
7+
cache: 'force-cache',
8+
next: { tags: ['thankyounext', 'justputit'] },
9+
}
10+
).then((res) => res.text())
11+
12+
return <p id="page-data">data: {data}</p>
13+
}

0 commit comments

Comments
 (0)