Skip to content

Commit 51c6a07

Browse files
shudingMaxLeiter
authored andcommitted
Fix module-level Server Action creation with closure-closed values (#62437)
With Server Actions, a module-level encryption can happen when you do: ```js function wrapAction(value) { return async function () { 'use server' console.log(value) } } const action = wrapAction('some-module-level-encryption-value') ``` ...as that action will be created when requiring this module, and it contains an encrypted argument from its closure (`value`). This currently throws an error during build: ``` Error: Missing manifest for Server Actions. This is a bug in Next.js at d (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/chunks/1772.js:1:15202) at f (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/chunks/1772.js:1:16917) at 714 (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/app/encryption/page.js:1:2806) at t (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/webpack-runtime.js:1:127) at 7940 (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/app/encryption/page.js:1:941) at t (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/webpack-runtime.js:1:127) at r (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/app/encryption/page.js:1:4529) at /Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/app/encryption/page.js:1:4572 at t.X (/Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/webpack-runtime.js:1:1181) at /Users/shu/Documents/git/next.js/test/e2e/app-dir/actions/.next/server/app/encryption/page.js:1:4542 ``` Because during module require phase, the encryption logic can't run as it doesn't have Server/Client references available yet (which are set during the rendering phase). Since both references are global singletons to the server and are already loaded early, this fix makes sure that they're registered via `setReferenceManifestsSingleton` before requiring the module. Closes NEXT-2579
1 parent 6e59c22 commit 51c6a07

File tree

6 files changed

+84
-26
lines changed

6 files changed

+84
-26
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { BuildManifest } from '../../server/get-page-files'
1111
import type { RequestData } from '../../server/web/types'
1212
import type { NextConfigComplete } from '../../server/config-shared'
1313
import { PAGE_TYPES } from '../../lib/page-types'
14+
import { setReferenceManifestsSingleton } from '../../server/app-render/action-encryption-utils'
15+
import { createServerModuleMap } from '../../server/app-render/action-utils'
1416

1517
declare const incrementalCacheHandler: any
1618
// OPTIONAL_IMPORT:incrementalCacheHandler
@@ -44,6 +46,17 @@ const subresourceIntegrityManifest = sriEnabled
4446
: undefined
4547
const nextFontManifest = maybeJSONParse(self.__NEXT_FONT_MANIFEST)
4648

49+
if (rscManifest && rscServerManifest) {
50+
setReferenceManifestsSingleton({
51+
clientReferenceManifest: rscManifest,
52+
serverActionsManifest: rscServerManifest,
53+
serverModuleMap: createServerModuleMap({
54+
serverActionsManifest: rscServerManifest,
55+
pageName: 'VAR_PAGE',
56+
}),
57+
})
58+
}
59+
4760
const render = getRender({
4861
pagesType: PAGE_TYPES.APP,
4962
dev,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin'
2+
3+
// This function creates a Flight-acceptable server module map proxy from our
4+
// Server Reference Manifest similar to our client module map.
5+
// This is because our manifest contains a lot of internal Next.js data that
6+
// are relevant to the runtime, workers, etc. that React doesn't need to know.
7+
export function createServerModuleMap({
8+
serverActionsManifest,
9+
pageName,
10+
}: {
11+
serverActionsManifest: ActionManifest
12+
pageName: string
13+
}) {
14+
return new Proxy(
15+
{},
16+
{
17+
get: (_, id: string) => {
18+
return {
19+
id: serverActionsManifest[
20+
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
21+
][id].workers['app' + pageName],
22+
name: id,
23+
chunks: [],
24+
}
25+
},
26+
}
27+
)
28+
}

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

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import { DetachedPromise } from '../../lib/detached-promise'
8080
import { isDynamicServerError } from '../../client/components/hooks-server-context'
8181
import { useFlightResponse } from './use-flight-response'
8282
import { isStaticGenBailoutError } from '../../client/components/static-generation-bailout'
83+
import { createServerModuleMap } from './action-utils'
8384

8485
export type GetDynamicParamFromSegment = (
8586
// [slug] / [[slug]] / [...slug]
@@ -586,27 +587,10 @@ async function renderToHTMLOrFlightImpl(
586587
// TODO: fix this typescript
587588
const clientReferenceManifest = renderOpts.clientReferenceManifest!
588589

589-
const workerName = 'app' + renderOpts.page
590-
const serverModuleMap: {
591-
[id: string]: {
592-
id: string
593-
chunks: string[]
594-
name: string
595-
}
596-
} = new Proxy(
597-
{},
598-
{
599-
get: (_, id: string) => {
600-
return {
601-
id: serverActionsManifest[
602-
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
603-
][id].workers[workerName],
604-
name: id,
605-
chunks: [],
606-
}
607-
},
608-
}
609-
)
590+
const serverModuleMap = createServerModuleMap({
591+
serverActionsManifest,
592+
pageName: renderOpts.page,
593+
})
610594

611595
setReferenceManifestsSingleton({
612596
clientReferenceManifest,

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from 'next/types'
1313
import type { RouteModule } from './future/route-modules/route-module'
1414
import type { BuildManifest } from './get-page-files'
15+
import type { ActionManifest } from '../build/webpack/plugins/flight-client-entry-plugin'
1516

1617
import {
1718
BUILD_MANIFEST,
@@ -26,6 +27,9 @@ import { getTracer } from './lib/trace/tracer'
2627
import { LoadComponentsSpan } from './lib/trace/constants'
2728
import { evalManifest, loadManifest } from './load-manifest'
2829
import { wait } from '../lib/wait'
30+
import { setReferenceManifestsSingleton } from './app-render/action-encryption-utils'
31+
import { createServerModuleMap } from './app-render/action-utils'
32+
2933
export type ManifestItem = {
3034
id: number | string
3135
files: string[]
@@ -132,15 +136,13 @@ async function loadComponentsImpl<N = any>({
132136
Promise.resolve().then(() => requirePage('/_app', distDir, false)),
133137
])
134138
}
135-
const ComponentMod = await Promise.resolve().then(() =>
136-
requirePage(page, distDir, isAppPath)
137-
)
138139

139140
// Make sure to avoid loading the manifest for Route Handlers
140141
const hasClientManifest =
141142
isAppPath &&
142143
(page.endsWith('/page') || page === '/not-found' || page === '/_not-found')
143144

145+
// Load the manifest files first
144146
const [
145147
buildManifest,
146148
reactLoadableManifest,
@@ -165,12 +167,30 @@ async function loadComponentsImpl<N = any>({
165167
)
166168
: undefined,
167169
isAppPath
168-
? loadManifestWithRetries(
170+
? (loadManifestWithRetries(
169171
join(distDir, 'server', SERVER_REFERENCE_MANIFEST + '.json')
170-
).catch(() => null)
172+
).catch(() => null) as Promise<ActionManifest | null>)
171173
: null,
172174
])
173175

176+
// Before requring the actual page module, we have to set the reference manifests
177+
// to our global store so Server Action's encryption util can access to them
178+
// at the top level of the page module.
179+
if (serverActionsManifest && clientReferenceManifest) {
180+
setReferenceManifestsSingleton({
181+
clientReferenceManifest,
182+
serverActionsManifest,
183+
serverModuleMap: createServerModuleMap({
184+
serverActionsManifest,
185+
pageName: page,
186+
}),
187+
})
188+
}
189+
190+
const ComponentMod = await Promise.resolve().then(() =>
191+
requirePage(page, distDir, isAppPath)
192+
)
193+
174194
const Component = interopDefault(ComponentMod)
175195
const Document = interopDefault(DocumentMod)
176196
const App = interopDefault(AppMod)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,7 @@ createNextDescribe(
981981
const res = await next.fetch('/encryption')
982982
const html = await res.text()
983983
expect(html).not.toContain('qwerty123')
984+
expect(html).not.toContain('some-module-level-encryption-value')
984985
})
985986
})
986987

test/e2e/app-dir/actions/app/encryption/page.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
// Test top-level encryption (happens during the module load phase)
2+
function wrapAction(value) {
3+
return async function () {
4+
'use server'
5+
console.log(value)
6+
}
7+
}
8+
9+
const action = wrapAction('some-module-level-encryption-value')
10+
11+
// Test runtime encryption (happens during the rendering phase)
112
export default function Page() {
213
const secret = 'my password is qwerty123'
314

@@ -6,6 +17,7 @@ export default function Page() {
617
action={async () => {
718
'use server'
819
console.log(secret)
20+
await action()
921
return 'success'
1022
}}
1123
>

0 commit comments

Comments
 (0)