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:
parent
301dd70ac7
commit
b0c6b00643
6 changed files with 84 additions and 26 deletions
|
@ -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,
|
||||
|
|
28
packages/next/src/server/app-render/action-utils.ts
Normal file
28
packages/next/src/server/app-render/action-utils.ts
Normal 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: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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'
|
||||
}}
|
||||
>
|
||||
|
|
Loading…
Reference in a new issue