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
This commit is contained in:
Shu Ding 2024-02-23 12:00:24 +01:00 committed by GitHub
parent 301dd70ac7
commit b0c6b00643
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 84 additions and 26 deletions

View file

@ -11,6 +11,8 @@ import type { BuildManifest } from '../../server/get-page-files'
import type { RequestData } from '../../server/web/types'
import type { NextConfigComplete } from '../../server/config-shared'
import { PAGE_TYPES } from '../../lib/page-types'
import { setReferenceManifestsSingleton } from '../../server/app-render/action-encryption-utils'
import { createServerModuleMap } from '../../server/app-render/action-utils'
declare const incrementalCacheHandler: any
// OPTIONAL_IMPORT:incrementalCacheHandler
@ -47,6 +49,17 @@ const nextFontManifest = maybeJSONParse(self.__NEXT_FONT_MANIFEST)
const interceptionRouteRewrites =
maybeJSONParse(self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST) ?? []
if (rscManifest && rscServerManifest) {
setReferenceManifestsSingleton({
clientReferenceManifest: rscManifest,
serverActionsManifest: rscServerManifest,
serverModuleMap: createServerModuleMap({
serverActionsManifest: rscServerManifest,
pageName: 'VAR_PAGE',
}),
})
}
const render = getRender({
pagesType: PAGE_TYPES.APP,
dev,

View file

@ -0,0 +1,28 @@
import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin'
// This function creates a Flight-acceptable server module map proxy from our
// Server Reference Manifest similar to our client module map.
// This is because our manifest contains a lot of internal Next.js data that
// are relevant to the runtime, workers, etc. that React doesn't need to know.
export function createServerModuleMap({
serverActionsManifest,
pageName,
}: {
serverActionsManifest: ActionManifest
pageName: string
}) {
return new Proxy(
{},
{
get: (_, id: string) => {
return {
id: serverActionsManifest[
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
][id].workers['app' + pageName],
name: id,
chunks: [],
}
},
}
)
}

View file

@ -106,6 +106,7 @@ import {
getClientComponentLoaderMetrics,
wrapClientComponentLoader,
} from '../client-component-renderer-logger'
import { createServerModuleMap } from './action-utils'
export type GetDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
@ -654,27 +655,10 @@ async function renderToHTMLOrFlightImpl(
// TODO: fix this typescript
const clientReferenceManifest = renderOpts.clientReferenceManifest!
const workerName = 'app' + renderOpts.page
const serverModuleMap: {
[id: string]: {
id: string
chunks: string[]
name: string
}
} = new Proxy(
{},
{
get: (_, id: string) => {
return {
id: serverActionsManifest[
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
][id].workers[workerName],
name: id,
chunks: [],
}
},
}
)
const serverModuleMap = createServerModuleMap({
serverActionsManifest,
pageName: renderOpts.page,
})
setReferenceManifestsSingleton({
clientReferenceManifest,

View file

@ -12,6 +12,7 @@ import type {
} from 'next/types'
import type { RouteModule } from './future/route-modules/route-module'
import type { BuildManifest } from './get-page-files'
import type { ActionManifest } from '../build/webpack/plugins/flight-client-entry-plugin'
import {
BUILD_MANIFEST,
@ -26,6 +27,9 @@ import { getTracer } from './lib/trace/tracer'
import { LoadComponentsSpan } from './lib/trace/constants'
import { evalManifest, loadManifest } from './load-manifest'
import { wait } from '../lib/wait'
import { setReferenceManifestsSingleton } from './app-render/action-encryption-utils'
import { createServerModuleMap } from './app-render/action-utils'
export type ManifestItem = {
id: number | string
files: string[]
@ -132,15 +136,13 @@ async function loadComponentsImpl<N = any>({
Promise.resolve().then(() => requirePage('/_app', distDir, false)),
])
}
const ComponentMod = await Promise.resolve().then(() =>
requirePage(page, distDir, isAppPath)
)
// Make sure to avoid loading the manifest for Route Handlers
const hasClientManifest =
isAppPath &&
(page.endsWith('/page') || page === '/not-found' || page === '/_not-found')
// Load the manifest files first
const [
buildManifest,
reactLoadableManifest,
@ -165,12 +167,30 @@ async function loadComponentsImpl<N = any>({
)
: undefined,
isAppPath
? loadManifestWithRetries(
? (loadManifestWithRetries(
join(distDir, 'server', SERVER_REFERENCE_MANIFEST + '.json')
).catch(() => null)
).catch(() => null) as Promise<ActionManifest | null>)
: null,
])
// Before requring the actual page module, we have to set the reference manifests
// to our global store so Server Action's encryption util can access to them
// at the top level of the page module.
if (serverActionsManifest && clientReferenceManifest) {
setReferenceManifestsSingleton({
clientReferenceManifest,
serverActionsManifest,
serverModuleMap: createServerModuleMap({
serverActionsManifest,
pageName: page,
}),
})
}
const ComponentMod = await Promise.resolve().then(() =>
requirePage(page, distDir, isAppPath)
)
const Component = interopDefault(ComponentMod)
const Document = interopDefault(DocumentMod)
const App = interopDefault(AppMod)

View file

@ -1026,6 +1026,7 @@ createNextDescribe(
const res = await next.fetch('/encryption')
const html = await res.text()
expect(html).not.toContain('qwerty123')
expect(html).not.toContain('some-module-level-encryption-value')
})
})

View file

@ -1,3 +1,14 @@
// Test top-level encryption (happens during the module load phase)
function wrapAction(value) {
return async function () {
'use server'
console.log(value)
}
}
const action = wrapAction('some-module-level-encryption-value')
// Test runtime encryption (happens during the rendering phase)
export default function Page() {
const secret = 'my password is qwerty123'
@ -6,6 +17,7 @@ export default function Page() {
action={async () => {
'use server'
console.log(secret)
await action()
return 'success'
}}
>