Skip to content

Commit b0c6b00

Browse files
authored
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 301dd70 commit b0c6b00

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

+13
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
@@ -47,6 +49,17 @@ const nextFontManifest = maybeJSONParse(self.__NEXT_FONT_MANIFEST)
4749
const interceptionRouteRewrites =
4850
maybeJSONParse(self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST) ?? []
4951

52+
if (rscManifest && rscServerManifest) {
53+
setReferenceManifestsSingleton({
54+
clientReferenceManifest: rscManifest,
55+
serverActionsManifest: rscServerManifest,
56+
serverModuleMap: createServerModuleMap({
57+
serverActionsManifest: rscServerManifest,
58+
pageName: 'VAR_PAGE',
59+
}),
60+
})
61+
}
62+
5063
const render = getRender({
5164
pagesType: PAGE_TYPES.APP,
5265
dev,
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

+5-21
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import {
106106
getClientComponentLoaderMetrics,
107107
wrapClientComponentLoader,
108108
} from '../client-component-renderer-logger'
109+
import { createServerModuleMap } from './action-utils'
109110

110111
export type GetDynamicParamFromSegment = (
111112
// [slug] / [[slug]] / [...slug]
@@ -654,27 +655,10 @@ async function renderToHTMLOrFlightImpl(
654655
// TODO: fix this typescript
655656
const clientReferenceManifest = renderOpts.clientReferenceManifest!
656657

657-
const workerName = 'app' + renderOpts.page
658-
const serverModuleMap: {
659-
[id: string]: {
660-
id: string
661-
chunks: string[]
662-
name: string
663-
}
664-
} = new Proxy(
665-
{},
666-
{
667-
get: (_, id: string) => {
668-
return {
669-
id: serverActionsManifest[
670-
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
671-
][id].workers[workerName],
672-
name: id,
673-
chunks: [],
674-
}
675-
},
676-
}
677-
)
658+
const serverModuleMap = createServerModuleMap({
659+
serverActionsManifest,
660+
pageName: renderOpts.page,
661+
})
678662

679663
setReferenceManifestsSingleton({
680664
clientReferenceManifest,

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

+25-5
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

+1
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,7 @@ createNextDescribe(
10261026
const res = await next.fetch('/encryption')
10271027
const html = await res.text()
10281028
expect(html).not.toContain('qwerty123')
1029+
expect(html).not.toContain('some-module-level-encryption-value')
10291030
})
10301031
})
10311032

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

+12
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)