Skip to content

Commit d71da77

Browse files
authored
Ignore an existing HMR refresh hash cookie with next start (#77714)
This is a follow-up fix for #75474. When switching from `next dev` to `next start` locally, a user might have an existing HMR refresh hash session cookie. We need to make sure that the hash value is excluded from any `"use cache"` cache keys, when running with `next start`. Otherwise, the cache keys will be different when prerendering vs. handling a dynamic request, e.g. when executing a server action. In production, we now also omit the serialized `"$undefined"` value for the HMR refresh hash from the cache keys to keep them shorter.
1 parent ff4ee00 commit d71da77

File tree

9 files changed

+282
-202
lines changed

9 files changed

+282
-202
lines changed

packages/next/src/server/app-render/work-unit-async-storage.external.ts

+5
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,13 @@ export function getRenderResumeDataCache(
269269
}
270270

271271
export function getHmrRefreshHash(
272+
workStore: WorkStore,
272273
workUnitStore: WorkUnitStore
273274
): string | undefined {
275+
if (!workStore.dev) {
276+
return undefined
277+
}
278+
274279
return workUnitStore.type === 'cache'
275280
? workUnitStore.hmrRefreshHash
276281
: workUnitStore.type === 'request'

packages/next/src/server/use-cache/use-cache-wrapper.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,9 @@ import type { Params } from '../request/params'
5353
import React from 'react'
5454
import type { ImplicitTags } from '../lib/implicit-tags'
5555

56-
type CacheKeyParts = [
57-
buildId: string,
58-
hmrRefreshHash: string | undefined,
59-
id: string,
60-
args: unknown[],
61-
]
56+
type CacheKeyParts =
57+
| [buildId: string, id: string, args: unknown[]]
58+
| [buildId: string, id: string, args: unknown[], hmrRefreshHash: string]
6259

6360
export interface UseCachePageComponentProps {
6461
params: Promise<Params>
@@ -162,7 +159,8 @@ function generateCacheEntryWithCacheContext(
162159
explicitExpire: undefined,
163160
explicitStale: undefined,
164161
tags: null,
165-
hmrRefreshHash: outerWorkUnitStore && getHmrRefreshHash(outerWorkUnitStore),
162+
hmrRefreshHash:
163+
outerWorkUnitStore && getHmrRefreshHash(workStore, outerWorkUnitStore),
166164
isHmrRefresh: useCacheOrRequestStore?.isHmrRefresh ?? false,
167165
serverComponentsHmrCache: useCacheOrRequestStore?.serverComponentsHmrCache,
168166
forceRevalidate: shouldForceRevalidate(workStore, outerWorkUnitStore),
@@ -309,7 +307,7 @@ async function generateCacheEntryImpl(
309307
): Promise<[ReadableStream, Promise<CacheEntry>]> {
310308
const temporaryReferences = createServerTemporaryReferenceSet()
311309

312-
const [, , , args] =
310+
const [, , args] =
313311
typeof encodedArguments === 'string'
314312
? await decodeReply<CacheKeyParts>(
315313
encodedArguments,
@@ -543,7 +541,8 @@ export function cache(
543541
// components have been edited. This is a very coarse approach. But it's
544542
// also only a temporary solution until Action IDs are unique per
545543
// implementation. Remove this once Action IDs hash the implementation.
546-
const hmrRefreshHash = workUnitStore && getHmrRefreshHash(workUnitStore)
544+
const hmrRefreshHash =
545+
workUnitStore && getHmrRefreshHash(workStore, workUnitStore)
547546

548547
const hangingInputAbortSignal =
549548
workUnitStore?.type === 'prerender'
@@ -605,7 +604,11 @@ export function cache(
605604
}
606605

607606
const temporaryReferences = createClientTemporaryReferenceSet()
608-
const cacheKeyParts: CacheKeyParts = [buildId, hmrRefreshHash, id, args]
607+
608+
const cacheKeyParts: CacheKeyParts = hmrRefreshHash
609+
? [buildId, id, args, hmrRefreshHash]
610+
: [buildId, id, args]
611+
609612
const encodedCacheKeyParts: FormData | string = await encodeReply(
610613
cacheKeyParts,
611614
{ temporaryReferences, signal: hangingInputAbortSignal }

test/development/app-dir/use-cache-dev/app/page.tsx

-28
This file was deleted.

test/development/app-dir/use-cache-dev/use-cache-dev.test.ts

-160
This file was deleted.

test/e2e/app-dir/use-cache-custom-handler/use-cache-custom-handler.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ describe('use-cache-custom-handler', () => {
3232
)
3333

3434
expect(next.cliOutput.slice(outputIndex)).toMatch(
35-
/ModernCustomCacheHandler::get \["(development|[A-Za-z0-9_-]{21})","\$undefined","([0-9a-f]{2})+",\[\]\]/
35+
/ModernCustomCacheHandler::get \["(development|[A-Za-z0-9_-]{21})","([0-9a-f]{2})+",\[\]\]/
3636
)
3737

3838
expect(next.cliOutput.slice(outputIndex)).toMatch(
39-
/ModernCustomCacheHandler::set \["(development|[A-Za-z0-9_-]{21})","\$undefined","([0-9a-f]{2})+",\[\]\]/
39+
/ModernCustomCacheHandler::set \["(development|[A-Za-z0-9_-]{21})","([0-9a-f]{2})+",\[\]\]/
4040
)
4141

4242
// The data should be cached initially.
@@ -67,11 +67,11 @@ describe('use-cache-custom-handler', () => {
6767
)
6868

6969
expect(next.cliOutput.slice(outputIndex)).toMatch(
70-
/LegacyCustomCacheHandler::get \["(development|[A-Za-z0-9_-]{21})","\$undefined","([0-9a-f]{2})+",\[\]\] \["_N_T_\/layout","_N_T_\/legacy\/layout","_N_T_\/legacy\/page","_N_T_\/legacy"\]/
70+
/LegacyCustomCacheHandler::get \["(development|[A-Za-z0-9_-]{21})","([0-9a-f]{2})+",\[\]\] \["_N_T_\/layout","_N_T_\/legacy\/layout","_N_T_\/legacy\/page","_N_T_\/legacy"\]/
7171
)
7272

7373
expect(next.cliOutput.slice(outputIndex)).toMatch(
74-
/LegacyCustomCacheHandler::set \["(development|[A-Za-z0-9_-]{21})","\$undefined","([0-9a-f]{2})+",\[\]\]/
74+
/LegacyCustomCacheHandler::set \["(development|[A-Za-z0-9_-]{21})","([0-9a-f]{2})+",\[\]\]/
7575
)
7676

7777
// The data should be cached initially.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { revalidatePath } from 'next/cache'
2+
3+
async function getRandomValue() {
4+
'use cache'
5+
6+
return Math.random()
7+
}
8+
9+
async function getData() {
10+
'use cache'
11+
12+
return fetch('https://next-data-api-endpoint.vercel.app/api/random').then(
13+
(res) =>
14+
res
15+
.text()
16+
.then(async (text) => [text, 'foo', await getRandomValue()] as const)
17+
)
18+
}
19+
20+
export default async function Page() {
21+
const [fetchedRandom, text, mathRandom] = await getData()
22+
23+
return (
24+
<>
25+
<div id="container">
26+
<p id="fetchedRandom">{fetchedRandom}</p>
27+
<p id="text">{text}</p>
28+
<p id="mathRandom">{mathRandom}</p>
29+
</div>
30+
<p id="uncached">{new Date().toISOString()}</p>
31+
<form
32+
action={async () => {
33+
'use server'
34+
revalidatePath('/')
35+
}}
36+
>
37+
<button id="revalidate">Revalidate</button>
38+
</form>
39+
</>
40+
)
41+
}

0 commit comments

Comments
 (0)