Skip to content

Commit 528a6b2

Browse files
committed
Handle cross-page client reference contamination in development
In development, React is tracking I/O for debugging purposes. Under some circumstances it can happen that I/O that is cached in a global (generally not recommended!) might be triggered by a component of one page (the owner of that I/O) and also emitted as I/O debug info for a component of another page that depends on it. If the owner references client components in its props, those couldn't be serialized for the other page because they don't exist in the client reference manifest of that page, leading to the following error: ``` Error: Could not find the module "<ID>" in the React Client Manifest. This is probably a bug in the React Server Components bundler. ``` To support that case, we're now looking for client references in manifests of other pages, but only in development mode. A better fix might be to pass a joint `debugBundlerConfig` option to React, alongside the normal `bundlerConfig` option, or something like that. But for now, this should suffice. Another option we considered but disregarded is a change in React to ignore missing client references when emitting them as debug info, and emitting an omission placeholder instead. But for the general case this would mask real bugs in the RSC bundler. fixes #85883
1 parent c93c268 commit 528a6b2

23 files changed

+350
-401
lines changed

packages/next/errors.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -947,5 +947,8 @@
947947
"946": "Failed to deserialize errors.",
948948
"947": "Expected `sendErrorsToBrowser` to be defined in renderOpts.",
949949
"948": "Failed to serialize errors.",
950-
"949": "Route %s errored during %s. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled.\n \nOriginal Error: %s"
950+
"949": "Route %s errored during %s. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled.\n \nOriginal Error: %s",
951+
"950": "The manifests singleton was not initialized.",
952+
"951": "The client reference manifest for route \"%s\" does not exist.",
953+
"952": "Cannot access \"%s\" without a work store."
951954
}

packages/next/src/build/templates/app-page.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ import {
2121
createOpaqueFallbackRouteParams,
2222
type OpaqueFallbackRouteParams,
2323
} from '../../server/request/fallback-params'
24-
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
24+
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
2525
import {
2626
isHtmlBotRequest,
2727
shouldServeStreamingMetadata,
2828
} from '../../server/lib/streaming-metadata'
29-
import { createServerModuleMap } from '../../server/app-render/action-utils'
3029
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
3130
import { getIsPossibleServerAction } from '../../server/lib/server-action-request-meta'
3231
import {
@@ -400,13 +399,10 @@ export async function handler(
400399
// set the reference manifests to our global store so Server Action's
401400
// encryption util can access to them at the top level of the page module.
402401
if (serverActionsManifest && clientReferenceManifest) {
403-
setReferenceManifestsSingleton({
402+
setManifestsSingleton({
404403
page: srcPage,
405404
clientReferenceManifest,
406405
serverActionsManifest,
407-
serverModuleMap: createServerModuleMap({
408-
serverActionsManifest,
409-
}),
410406
})
411407
}
412408

@@ -540,8 +536,6 @@ export async function handler(
540536
nextFontManifest,
541537
reactLoadableManifest,
542538
subresourceIntegrityManifest,
543-
serverActionsManifest,
544-
clientReferenceManifest,
545539
setCacheStatus: routerServerContext?.setCacheStatus,
546540
setIsrStatus: routerServerContext?.setIsrStatus,
547541
setReactDebugChannel: routerServerContext?.setReactDebugChannel,

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { patchFetch as _patchFetch } from '../../server/lib/patch-fetch'
88
import type { IncomingMessage, ServerResponse } from 'node:http'
99
import { addRequestMeta, getRequestMeta } from '../../server/request-meta'
1010
import { getTracer, type Span, SpanKind } from '../../server/lib/trace/tracer'
11-
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
12-
import { createServerModuleMap } from '../../server/app-render/action-utils'
11+
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
1312
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
1413
import { NodeNextRequest, NodeNextResponse } from '../../server/base-http/node'
1514
import {
@@ -185,13 +184,10 @@ export async function handler(
185184
// set the reference manifests to our global store so Server Action's
186185
// encryption util can access to them at the top level of the page module.
187186
if (serverActionsManifest && clientReferenceManifest) {
188-
setReferenceManifestsSingleton({
187+
setManifestsSingleton({
189188
page: srcPage,
190189
clientReferenceManifest,
191190
serverActionsManifest,
192-
serverModuleMap: createServerModuleMap({
193-
serverActionsManifest,
194-
}),
195191
})
196192
}
197193

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createServerModuleMap } from '../../server/app-render/action-utils'
2-
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
1+
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
32
import type { NextConfigComplete } from '../../server/config-shared'
43
import { EdgeRouteModuleWrapper } from '../../server/web/edge-route-module-wrapper'
54

@@ -16,13 +15,10 @@ const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE']
1615
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
1716

1817
if (rscManifest && rscServerManifest) {
19-
setReferenceManifestsSingleton({
18+
setManifestsSingleton({
2019
page: 'VAR_PAGE',
2120
clientReferenceManifest: rscManifest,
2221
serverActionsManifest: rscServerManifest,
23-
serverModuleMap: createServerModuleMap({
24-
serverActionsManifest: rscServerManifest,
25-
}),
2622
})
2723
}
2824

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import * as pageMod from 'VAR_USERLAND'
66

77
import type { RequestData } from '../../server/web/types'
88
import type { NextConfigComplete } from '../../server/config-shared'
9-
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
10-
import { createServerModuleMap } from '../../server/app-render/action-utils'
9+
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
1110
import { initializeCacheHandlers } from '../../server/use-cache/handlers'
1211
import { BaseServerSpan } from '../../server/lib/trace/constants'
1312
import { getTracer, SpanKind, type Span } from '../../server/lib/trace/tracer'
@@ -40,13 +39,10 @@ const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE']
4039
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
4140

4241
if (rscManifest && rscServerManifest) {
43-
setReferenceManifestsSingleton({
42+
setManifestsSingleton({
4443
page: 'VAR_PAGE',
4544
clientReferenceManifest: rscManifest,
4645
serverActionsManifest: rscServerManifest,
47-
serverModuleMap: createServerModuleMap({
48-
serverActionsManifest: rscServerManifest,
49-
}),
5046
})
5147
}
5248

@@ -81,12 +77,10 @@ async function requestHandler(
8177
buildManifest,
8278
prerenderManifest,
8379
reactLoadableManifest,
84-
clientReferenceManifest,
8580
subresourceIntegrityManifest,
8681
dynamicCssManifest,
8782
nextFontManifest,
8883
resolvedPathname,
89-
serverActionsManifest,
9084
interceptionRoutePatterns,
9185
routerServerContext,
9286
} = prepareResult
@@ -129,8 +123,6 @@ async function requestHandler(
129123
reactLoadableManifest,
130124
subresourceIntegrityManifest,
131125
dynamicCssManifest,
132-
serverActionsManifest,
133-
clientReferenceManifest,
134126
setIsrStatus: routerServerContext?.setIsrStatus,
135127

136128
dir: pageRouteModule.relativeProjectDir,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ interface Options {
4141
type ModuleId = string | number /*| null*/
4242

4343
// double indexed chunkId, filename
44-
export type ManifestChunks = Array<string>
44+
export type ManifestChunks = ReadonlyArray<string>
4545

4646
const pluginState = getProxiedPluginState({
4747
ssrModules: {} as { [ssrModuleId: string]: ModuleInfo },

packages/next/src/export/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,6 @@ async function exportAppImpl(
385385
join(distDir, 'server', `${NEXT_FONT_MANIFEST}.json`)
386386
),
387387
images: nextConfig.images,
388-
...(enabledDirectories.app
389-
? {
390-
serverActionsManifest,
391-
}
392-
: {}),
393388
deploymentId: nextConfig.deploymentId,
394389
htmlLimitedBots: nextConfig.htmlLimitedBots.source,
395390
experimental: {

packages/next/src/server/app-render/action-handler.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { warn } from '../../build/output/log'
4949
import { RequestCookies, ResponseCookies } from '../web/spec-extension/cookies'
5050
import { HeadersAdapter } from '../web/spec-extension/adapters/headers'
5151
import { fromNodeOutgoingHttpHeaders } from '../web/utils'
52-
import { selectWorkerForForwarding } from './action-utils'
52+
import { selectWorkerForForwarding, type ServerModuleMap } from './action-utils'
5353
import { isNodeNextRequest, isWebNextRequest } from '../base-http/helpers'
5454
import { RedirectStatusCode } from '../../client/components/redirect-status-code'
5555
import { synchronizeMutableCookies } from '../async-storage/request-store'
@@ -59,7 +59,7 @@ import { InvariantError } from '../../shared/lib/invariant-error'
5959
import { executeRevalidates } from '../revalidation-utils'
6060
import { getRequestMeta } from '../request-meta'
6161
import { setCacheBustingSearchParam } from '../../client/components/router-reducer/set-cache-busting-search-param'
62-
import { getServerModuleMap } from './encryption-utils'
62+
import { getServerModuleMap } from './manifests-singleton'
6363

6464
function formDataFromSearchQueryString(query: string) {
6565
const searchParams = new URLSearchParams(query)
@@ -473,15 +473,6 @@ export function parseHostHeader(
473473
: undefined
474474
}
475475

476-
type ServerModuleMap = Record<
477-
string,
478-
{
479-
id: string
480-
chunks: string[]
481-
name: string
482-
}
483-
>
484-
485476
type ServerActionsConfig = {
486477
bodySizeLimit?: SizeLimit
487478
allowedOrigins?: string[]
@@ -522,7 +513,7 @@ export async function handleAction({
522513
metadata: AppPageRenderResultMetadata
523514
}): Promise<HandleActionResult> {
524515
const contentType = req.headers['content-type']
525-
const { serverActionsManifest, page } = ctx.renderOpts
516+
const { page } = ctx.renderOpts
526517
const serverModuleMap = getServerModuleMap()
527518

528519
const {
@@ -640,11 +631,7 @@ export async function handleAction({
640631
const actionWasForwarded = Boolean(req.headers['x-action-forwarded'])
641632

642633
if (actionId) {
643-
const forwardedWorker = selectWorkerForForwarding(
644-
actionId,
645-
page,
646-
serverActionsManifest
647-
)
634+
const forwardedWorker = selectWorkerForForwarding(actionId, page)
648635

649636
// If forwardedWorker is truthy, it means there isn't a worker for the action
650637
// in the current handler, so we forward the request to a worker that has the action.
@@ -685,7 +672,7 @@ export async function handleAction({
685672
{ isAction: true },
686673
async (): Promise<HandleActionResult> => {
687674
// We only use these for fetch actions -- MPA actions handle them inside `decodeAction`.
688-
let actionModId: string | undefined
675+
let actionModId: string | number | undefined
689676
let boundActionArguments: unknown[] = []
690677

691678
if (
@@ -1214,7 +1201,7 @@ async function executeActionAndPrepareForRender<
12141201
function getActionModIdOrError(
12151202
actionId: string | null,
12161203
serverModuleMap: ServerModuleMap
1217-
): string {
1204+
): string | number {
12181205
// if we're missing the action ID header, we can't do any further processing
12191206
if (!actionId) {
12201207
throw new InvariantError("Missing 'next-action' header.")

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ import type { ActionManifest } from '../../build/webpack/plugins/flight-client-e
22
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
33
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
44
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
5+
import { getServerActionsManifest } from './manifests-singleton'
56
import { workAsyncStorage } from './work-async-storage.external'
67

8+
export interface ServerModuleMap {
9+
readonly [name: string]: {
10+
readonly id: string | number
11+
readonly name: string
12+
readonly chunks: Readonly<Array<string>> // currently not used
13+
readonly async?: boolean
14+
}
15+
}
16+
717
// This function creates a Flight-acceptable server module map proxy from our
818
// Server Reference Manifest similar to our client module map.
919
// This is because our manifest contains a lot of internal Next.js data that
@@ -12,7 +22,7 @@ export function createServerModuleMap({
1222
serverActionsManifest,
1323
}: {
1424
serverActionsManifest: ActionManifest
15-
}) {
25+
}): ServerModuleMap {
1626
return new Proxy(
1727
{},
1828
{
@@ -61,11 +71,8 @@ export function createServerModuleMap({
6171
* Checks if the requested action has a worker for the current page.
6272
* If not, it returns the first worker that has a handler for the action.
6373
*/
64-
export function selectWorkerForForwarding(
65-
actionId: string,
66-
pageName: string,
67-
serverActionsManifest: ActionManifest
68-
) {
74+
export function selectWorkerForForwarding(actionId: string, pageName: string) {
75+
const serverActionsManifest = getServerActionsManifest()
6976
const workers =
7077
serverActionsManifest[
7178
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'

0 commit comments

Comments
 (0)