Skip to content

Commit 3265c95

Browse files
authored
Fix race condition when setting client reference manifests (#71741)
Similar to how #71669 fixed a race condition when setting the manifests singleton regarding the server action manifests, this PR fixes the same race condition for the client reference manifests.
1 parent 627a5ff commit 3265c95

File tree

13 files changed

+162
-66
lines changed

13 files changed

+162
-66
lines changed

packages/next/src/build/templates/edge-app-route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
1717

1818
if (rscManifest && rscServerManifest) {
1919
setReferenceManifestsSingleton({
20+
page: 'VAR_PAGE',
2021
clientReferenceManifest: rscManifest,
2122
serverActionsManifest: rscServerManifest,
2223
serverModuleMap: createServerModuleMap({

packages/next/src/build/templates/edge-ssr-app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const interceptionRouteRewrites =
5656

5757
if (rscManifest && rscServerManifest) {
5858
setReferenceManifestsSingleton({
59+
page: 'VAR_PAGE',
5960
clientReferenceManifest: rscManifest,
6061
serverActionsManifest: rscServerManifest,
6162
serverModuleMap: createServerModuleMap({

packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,21 @@ export interface ManifestNode {
7272
}
7373
}
7474

75-
export type ClientReferenceManifest = {
75+
export interface ClientReferenceManifestForRsc {
76+
clientModules: ManifestNode
77+
rscModuleMapping: {
78+
[moduleId: string]: ManifestNode
79+
}
80+
edgeRscModuleMapping: {
81+
[moduleId: string]: ManifestNode
82+
}
83+
}
84+
85+
export interface ClientReferenceManifest extends ClientReferenceManifestForRsc {
7686
readonly moduleLoading: {
7787
prefix: string
7888
crossOrigin: string | null
7989
}
80-
clientModules: ManifestNode
8190
ssrModuleMapping: {
8291
[moduleId: string]: ManifestNode
8392
}
@@ -90,12 +99,6 @@ export type ClientReferenceManifest = {
9099
entryJSFiles?: {
91100
[entry: string]: string[]
92101
}
93-
rscModuleMapping: {
94-
[moduleId: string]: ManifestNode
95-
}
96-
edgeRscModuleMapping: {
97-
[moduleId: string]: ManifestNode
98-
}
99102
}
100103

101104
function getAppPathRequiredChunks(

packages/next/src/server/app-render/app-render.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,7 @@ async function renderToHTMLOrFlightImpl(
10601060
const serverModuleMap = createServerModuleMap({ serverActionsManifest })
10611061

10621062
setReferenceManifestsSingleton({
1063+
page: workStore.page,
10631064
clientReferenceManifest,
10641065
serverActionsManifest,
10651066
serverModuleMap,

packages/next/src/server/app-render/encryption-utils.ts

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin'
2-
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
2+
import type {
3+
ClientReferenceManifest,
4+
ClientReferenceManifestForRsc,
5+
} from '../../build/webpack/plugins/flight-manifest-plugin'
36
import type { DeepReadonly } from '../../shared/lib/deep-readonly'
7+
import { InvariantError } from '../../shared/lib/invariant-error'
8+
import { workAsyncStorage } from './work-async-storage.external'
49

510
let __next_loaded_action_key: CryptoKey
611

@@ -64,10 +69,12 @@ const SERVER_ACTION_MANIFESTS_SINGLETON = Symbol.for(
6469
)
6570

6671
export function setReferenceManifestsSingleton({
72+
page,
6773
clientReferenceManifest,
6874
serverActionsManifest,
6975
serverModuleMap,
7076
}: {
77+
page: string
7178
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
7279
serverActionsManifest: DeepReadonly<ActionManifest>
7380
serverModuleMap: {
@@ -78,9 +85,19 @@ export function setReferenceManifestsSingleton({
7885
}
7986
}
8087
}) {
81-
// @ts-ignore
88+
// @ts-expect-error
89+
const clientReferenceManifestsPerPage = globalThis[
90+
SERVER_ACTION_MANIFESTS_SINGLETON
91+
]?.clientReferenceManifestsPerPage as
92+
| undefined
93+
| DeepReadonly<Record<string, ClientReferenceManifest>>
94+
95+
// @ts-expect-error
8296
globalThis[SERVER_ACTION_MANIFESTS_SINGLETON] = {
83-
clientReferenceManifest,
97+
clientReferenceManifestsPerPage: {
98+
...clientReferenceManifestsPerPage,
99+
[normalizePage(page)]: clientReferenceManifest,
100+
},
84101
serverActionsManifest,
85102
serverModuleMap,
86103
}
@@ -100,29 +117,49 @@ export function getServerModuleMap() {
100117
}
101118

102119
if (!serverActionsManifestSingleton) {
103-
throw new Error(
104-
'Missing manifest for Server Actions. This is a bug in Next.js'
105-
)
120+
throw new InvariantError('Missing manifest for Server Actions.')
106121
}
107122

108123
return serverActionsManifestSingleton.serverModuleMap
109124
}
110125

111-
export function getClientReferenceManifestSingleton() {
126+
export function getClientReferenceManifestForRsc(): DeepReadonly<ClientReferenceManifestForRsc> {
112127
const serverActionsManifestSingleton = (globalThis as any)[
113128
SERVER_ACTION_MANIFESTS_SINGLETON
114129
] as {
115-
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
116-
serverActionsManifest: DeepReadonly<ActionManifest>
130+
clientReferenceManifestsPerPage: DeepReadonly<
131+
Record<string, ClientReferenceManifest>
132+
>
117133
}
118134

119135
if (!serverActionsManifestSingleton) {
120-
throw new Error(
121-
'Missing manifest for Server Actions. This is a bug in Next.js'
122-
)
136+
throw new InvariantError('Missing manifest for Server Actions.')
137+
}
138+
139+
const { clientReferenceManifestsPerPage } = serverActionsManifestSingleton
140+
const workStore = workAsyncStorage.getStore()
141+
142+
if (!workStore) {
143+
// If there's no work store defined, we can assume that a client reference
144+
// manifest is needed during module evaluation, e.g. to create a server
145+
// action using a higher-order function. This might also use client
146+
// components which need to be serialized by Flight, and therefore client
147+
// references need to be resolvable. To make this work, we're returning a
148+
// merged manifest across all pages. This is fine as long as the module IDs
149+
// are not page specific, which they are not for Webpack. TODO: Fix this in
150+
// Turbopack.
151+
return mergeClientReferenceManifests(clientReferenceManifestsPerPage)
123152
}
124153

125-
return serverActionsManifestSingleton.clientReferenceManifest
154+
const page = normalizePage(workStore.page)
155+
156+
const clientReferenceManifest = clientReferenceManifestsPerPage[page]
157+
158+
if (!clientReferenceManifest) {
159+
throw new InvariantError(`Missing Client Reference Manifest for ${page}.`)
160+
}
161+
162+
return clientReferenceManifest
126163
}
127164

128165
export async function getActionEncryptionKey() {
@@ -133,22 +170,19 @@ export async function getActionEncryptionKey() {
133170
const serverActionsManifestSingleton = (globalThis as any)[
134171
SERVER_ACTION_MANIFESTS_SINGLETON
135172
] as {
136-
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
137173
serverActionsManifest: DeepReadonly<ActionManifest>
138174
}
139175

140176
if (!serverActionsManifestSingleton) {
141-
throw new Error(
142-
'Missing manifest for Server Actions. This is a bug in Next.js'
143-
)
177+
throw new InvariantError('Missing manifest for Server Actions.')
144178
}
145179

146180
const rawKey =
147181
process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY ||
148182
serverActionsManifestSingleton.serverActionsManifest.encryptionKey
149183

150184
if (rawKey === undefined) {
151-
throw new Error('Missing encryption key for Server Actions')
185+
throw new InvariantError('Missing encryption key for Server Actions')
152186
}
153187

154188
__next_loaded_action_key = await crypto.subtle.importKey(
@@ -161,3 +195,40 @@ export async function getActionEncryptionKey() {
161195

162196
return __next_loaded_action_key
163197
}
198+
199+
function normalizePage(page: string): string {
200+
return page.replace(/\/(page|route)$/, '')
201+
}
202+
203+
function mergeClientReferenceManifests(
204+
clientReferenceManifestsPerPage: DeepReadonly<
205+
Record<string, ClientReferenceManifest>
206+
>
207+
): ClientReferenceManifestForRsc {
208+
const clientReferenceManifests = Object.values(
209+
clientReferenceManifestsPerPage as Record<string, ClientReferenceManifest>
210+
)
211+
212+
const mergedClientReferenceManifest: ClientReferenceManifestForRsc = {
213+
clientModules: {},
214+
edgeRscModuleMapping: {},
215+
rscModuleMapping: {},
216+
}
217+
218+
for (const clientReferenceManifest of clientReferenceManifests) {
219+
mergedClientReferenceManifest.clientModules = {
220+
...mergedClientReferenceManifest.clientModules,
221+
...clientReferenceManifest.clientModules,
222+
}
223+
mergedClientReferenceManifest.edgeRscModuleMapping = {
224+
...mergedClientReferenceManifest.edgeRscModuleMapping,
225+
...clientReferenceManifest.edgeRscModuleMapping,
226+
}
227+
mergedClientReferenceManifest.rscModuleMapping = {
228+
...mergedClientReferenceManifest.rscModuleMapping,
229+
...clientReferenceManifest.rscModuleMapping,
230+
}
231+
}
232+
233+
return mergedClientReferenceManifest
234+
}

packages/next/src/server/app-render/encryption.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
decrypt,
1313
encrypt,
1414
getActionEncryptionKey,
15-
getClientReferenceManifestSingleton,
15+
getClientReferenceManifestForRsc,
1616
getServerModuleMap,
1717
stringToUint8Array,
1818
} from './encryption-utils'
@@ -70,11 +70,11 @@ async function encodeActionBoundArg(actionId: string, arg: string) {
7070

7171
// Encrypts the action's bound args into a string.
7272
export async function encryptActionBoundArgs(actionId: string, args: any[]) {
73-
const clientReferenceManifestSingleton = getClientReferenceManifestSingleton()
73+
const { clientModules } = getClientReferenceManifestForRsc()
7474

7575
// Using Flight to serialize the args into a string.
7676
const serialized = await streamToString(
77-
renderToReadableStream(args, clientReferenceManifestSingleton.clientModules)
77+
renderToReadableStream(args, clientModules)
7878
)
7979

8080
// Encrypt the serialized string with the action id as the salt.
@@ -90,16 +90,17 @@ export async function decryptActionBoundArgs(
9090
actionId: string,
9191
encrypted: Promise<string>
9292
) {
93-
const clientReferenceManifestSingleton = getClientReferenceManifestSingleton()
93+
const { edgeRscModuleMapping, rscModuleMapping } =
94+
getClientReferenceManifestForRsc()
9495

9596
// Decrypt the serialized string with the action id as the salt.
96-
const decryped = await decodeActionBoundArg(actionId, await encrypted)
97+
const decrypted = await decodeActionBoundArg(actionId, await encrypted)
9798

9899
// Using Flight to deserialize the args from the string.
99100
const deserialized = await createFromReadableStream(
100101
new ReadableStream({
101102
start(controller) {
102-
controller.enqueue(textEncoder.encode(decryped))
103+
controller.enqueue(textEncoder.encode(decrypted))
103104
controller.close()
104105
},
105106
}),
@@ -109,9 +110,7 @@ export async function decryptActionBoundArgs(
109110
// to be added to the current execution. Instead, we'll wait for any ClientReference
110111
// to be emitted which themselves will handle the preloading.
111112
moduleLoading: null,
112-
moduleMap: isEdgeRuntime
113-
? clientReferenceManifestSingleton.edgeRscModuleMapping
114-
: clientReferenceManifestSingleton.rscModuleMapping,
113+
moduleMap: isEdgeRuntime ? edgeRscModuleMapping : rscModuleMapping,
115114
serverModuleMap: getServerModuleMap(),
116115
},
117116
}

packages/next/src/server/load-components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ async function loadComponentsImpl<N = any>({
177177
// to them at the top level of the page module.
178178
if (serverActionsManifest && clientReferenceManifest) {
179179
setReferenceManifestsSingleton({
180+
page,
180181
clientReferenceManifest,
181182
serverActionsManifest,
182183
serverModuleMap: createServerModuleMap({

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import { makeHangingPromise } from '../dynamic-rendering-utils'
2525

2626
import { cacheScopeAsyncLocalStorage } from '../async-storage/cache-scope.external'
2727

28-
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
28+
import type { ClientReferenceManifestForRsc } from '../../build/webpack/plugins/flight-manifest-plugin'
2929

3030
import {
31-
getClientReferenceManifestSingleton,
31+
getClientReferenceManifestForRsc,
3232
getServerModuleMap,
3333
} from '../app-render/encryption-utils'
3434
import type { CacheScopeStore } from '../async-storage/cache-scope.external'
@@ -66,7 +66,7 @@ function generateCacheEntry(
6666
workStore: WorkStore,
6767
outerWorkUnitStore: WorkUnitStore | undefined,
6868
cacheScope: undefined | CacheScopeStore,
69-
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
69+
clientReferenceManifest: DeepReadonly<ClientReferenceManifestForRsc>,
7070
encodedArguments: FormData | string,
7171
fn: any
7272
): Promise<[ReadableStream, Promise<CacheEntry>]> {
@@ -90,7 +90,7 @@ function generateCacheEntryWithRestoredWorkStore(
9090
workStore: WorkStore,
9191
outerWorkUnitStore: WorkUnitStore | undefined,
9292
cacheScope: undefined | CacheScopeStore,
93-
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
93+
clientReferenceManifest: DeepReadonly<ClientReferenceManifestForRsc>,
9494
encodedArguments: FormData | string,
9595
fn: any
9696
) {
@@ -128,7 +128,7 @@ function generateCacheEntryWithRestoredWorkStore(
128128
function generateCacheEntryWithCacheContext(
129129
workStore: WorkStore,
130130
outerWorkUnitStore: WorkUnitStore | undefined,
131-
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
131+
clientReferenceManifest: DeepReadonly<ClientReferenceManifestForRsc>,
132132
encodedArguments: FormData | string,
133133
fn: any
134134
) {
@@ -298,7 +298,7 @@ async function generateCacheEntryImpl(
298298
workStore: WorkStore,
299299
outerWorkUnitStore: WorkUnitStore | undefined,
300300
innerCacheStore: UseCacheStore,
301-
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
301+
clientReferenceManifest: DeepReadonly<ClientReferenceManifestForRsc>,
302302
encodedArguments: FormData | string,
303303
fn: any
304304
): Promise<[ReadableStream, Promise<CacheEntry>]> {
@@ -455,7 +455,7 @@ export function cache(kind: string, id: string, fn: any) {
455455

456456
// Get the clientReferenceManifest while we're still in the outer Context.
457457
// In case getClientReferenceManifestSingleton is implemented using AsyncLocalStorage.
458-
const clientReferenceManifest = getClientReferenceManifestSingleton()
458+
const clientReferenceManifest = getClientReferenceManifestForRsc()
459459

460460
// Because the Action ID is not yet unique per implementation of that Action we can't
461461
// safely reuse the results across builds yet. In the meantime we add the buildId to the

test/e2e/app-dir/actions/app-action.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,16 @@ describe('app-dir action handling', () => {
15871587
expect(html).not.toContain('qwerty123')
15881588
expect(html).not.toContain('some-module-level-encryption-value')
15891589
})
1590+
1591+
it('should be able to resolve other server actions and client components', async () => {
1592+
const browser = await next.browser('/encryption')
1593+
expect(await browser.elementByCss('p').text()).toBe('initial')
1594+
await browser.elementByCss('button').click()
1595+
1596+
await retry(async () => {
1597+
expect(await browser.elementByCss('p').text()).toBe('hello from client')
1598+
})
1599+
})
15901600
})
15911601

15921602
describe('redirects', () => {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use client'
2+
3+
export function Client() {
4+
return 'hello from client'
5+
}

0 commit comments

Comments
 (0)