Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 1 addition & 17 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,6 @@ async function prospectiveRuntimeServerPrerender(
renderResumeDataCache,
prerenderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined,
// We only need task sequencing in the final prerender.
runtimeStagePromise: null,
// These are not present in regular prerenders, but allowed in a runtime prerender.
Expand Down Expand Up @@ -1092,7 +1091,6 @@ async function finalRuntimeServerPrerender(
prerenderResumeDataCache,
renderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined,
// Used to separate the "Static" stage from the "Runtime" stage.
runtimeStagePromise,
// These are not present in regular prerenders, but allowed in a runtime prerender.
Expand Down Expand Up @@ -3315,9 +3313,7 @@ async function spawnDynamicValidationInDev(
// ready to cut the render off.
const cacheSignal = new CacheSignal()

const captureOwnerStackClient = ReactClient.captureOwnerStack
const { captureOwnerStack: captureOwnerStackServer, createElement } =
ComponentMod
const { createElement } = ComponentMod

// The resume data cache here should use a fresh instance as it's
// performing a fresh prerender. If we get to implementing the
Expand Down Expand Up @@ -3350,7 +3346,6 @@ async function spawnDynamicValidationInDev(
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash,
captureOwnerStack: captureOwnerStackServer,
}

// We're not going to use the result of this render because the only time it could be used
Expand Down Expand Up @@ -3384,7 +3379,6 @@ async function spawnDynamicValidationInDev(
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash,
captureOwnerStack: captureOwnerStackServer,
}

const pendingInitialServerResult = workUnitAsyncStorage.run(
Expand Down Expand Up @@ -3505,7 +3499,6 @@ async function spawnDynamicValidationInDev(
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash: undefined,
captureOwnerStack: captureOwnerStackClient,
}

const prerender = (
Expand Down Expand Up @@ -3616,7 +3609,6 @@ async function spawnDynamicValidationInDev(
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash,
captureOwnerStack: captureOwnerStackServer,
}

const finalAttemptRSCPayload = await workUnitAsyncStorage.run(
Expand Down Expand Up @@ -3650,7 +3642,6 @@ async function spawnDynamicValidationInDev(
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash,
captureOwnerStack: captureOwnerStackServer,
}

const reactServerResult = await createReactServerPrerenderResult(
Expand Down Expand Up @@ -3730,7 +3721,6 @@ async function spawnDynamicValidationInDev(
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash,
captureOwnerStack: captureOwnerStackClient,
}

let dynamicValidation = createDynamicValidationState()
Expand Down Expand Up @@ -4108,7 +4098,6 @@ async function prerenderToStream(
prerenderResumeDataCache,
renderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined, // Not available in production.
}

// We're not going to use the result of this render because the only time it could be used
Expand Down Expand Up @@ -4142,7 +4131,6 @@ async function prerenderToStream(
prerenderResumeDataCache,
renderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined, // Not available in production.
})

const pendingInitialServerResult = workUnitAsyncStorage.run(
Expand Down Expand Up @@ -4257,7 +4245,6 @@ async function prerenderToStream(
prerenderResumeDataCache,
renderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined, // Not available in production.
}

const prerender = (
Expand Down Expand Up @@ -4367,7 +4354,6 @@ async function prerenderToStream(
prerenderResumeDataCache,
renderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined, // Not available in production.
}

const finalAttemptRSCPayload = await workUnitAsyncStorage.run(
Expand Down Expand Up @@ -4402,7 +4388,6 @@ async function prerenderToStream(
prerenderResumeDataCache,
renderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined, // Not available in production.
})

let prerenderIsPending = true
Expand Down Expand Up @@ -4488,7 +4473,6 @@ async function prerenderToStream(
prerenderResumeDataCache,
renderResumeDataCache,
hmrRefreshHash: undefined,
captureOwnerStack: undefined, // Not available in production.
}

let dynamicValidation = createDynamicValidationState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,6 @@ interface PrerenderStoreModernCommon
* subsequent dynamic render.
*/
readonly hmrRefreshHash: string | undefined

/**
* Only available in dev mode.
*/
readonly captureOwnerStack: undefined | (() => string | null)
}

interface StaticPrerenderStoreCommon {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,23 @@ describe('console-exit patches', () => {
}

// Install patches - this wraps the current console.log
const {
registerGetCacheSignal,
} = require('next/dist/server/node-environment-extensions/console-dim.external')
require('next/dist/server/node-environment-extensions/console-dim.external')

registerGetCacheSignal(() => null)
registerGetCacheSignal(() => controller?.signal)
registerGetCacheSignal(() => null)
const {
registerServerReact,
registerClientReact,
} = require('next/dist/server/runtime-reacts.external')

registerServerReact({
cacheSignal() {
return controller?.signal
},
})
registerClientReact({
cacheSignal() {
return null
},
})

console.log('before abort')
controller.abort()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import {
type ConsoleStore,
} from '../app-render/console-async-storage.external'
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'

type GetCacheSignal = () => AbortSignal | null
const cacheSignals: Array<GetCacheSignal> = []
export function registerGetCacheSignal(getSignal: GetCacheSignal): void {
cacheSignals.push(getSignal)
}
import { getServerReact, getClientReact } from '../runtime-reacts.external'

// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we may use later and want parity with the HIDDEN_STYLE value
const DIMMED_STYLE = 'dimmed'
Expand Down Expand Up @@ -191,33 +186,32 @@ function patchConsoleMethod(methodName: InterceptableConsoleMethod): void {
// the server React cacheSignal implementation. Any particular console call will be in one, the other, or neither
// scope and these signals return null if you are out of scope so this can be called from a single global patch
// and still work properly.
for (let i = 0; i < cacheSignals.length; i++) {
const signal = cacheSignals[i]() // try to get a signal from registered functions
if (signal) {
// We are in a React Server render and can consult the React cache signal to determine if logs
// are now dimmable.
if (signal.aborted) {
if (currentAbortedLogsStyle === HIDDEN_STYLE) {
return
}
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else if (consoleStore?.dim === true) {
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else {
return originalMethod.apply(this, args)
const signal =
getClientReact()?.cacheSignal() ?? getServerReact()?.cacheSignal()
if (signal) {
// We are in a React Server render and can consult the React cache signal to determine if logs
// are now dimmable.
if (signal.aborted) {
if (currentAbortedLogsStyle === HIDDEN_STYLE) {
return
}
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else if (consoleStore?.dim === true) {
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else {
return originalMethod.apply(this, args)
}
Comment on lines +189 to 215
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React cacheSignal API version introduced when stable

💡 Result:

cacheSignal was introduced as a stable React API in React 19.2.0, released October 1, 2025. [1], [2]

Stable version: react@19.2.0 (and later). [2], [3]

Docs: It’s documented in the React API reference under cacheSignal. [3]

Sources:
[1] React 19.2 release post (Oct 1, 2025) (react.dev)
[2] React changelog / release notes for 19.2.0 (collected.press)
[3] React API reference page for cacheSignal (react.dev)

Citations:


🏁 Script executed:

# First, let's examine the actual code file to understand the context
cat -n packages/next/src/server/node-environment-extensions/console-dim.external.tsx | head -250 | tail -100

Repository: Signal65/next.js-CodeRabbit

Length of output: 4802


🏁 Script executed:

# Search for getClientReact and getServerReact definitions
rg -n "getClientReact|getServerReact" packages/next/src/server/node-environment-extensions/console-dim.external.tsx -A 5

Repository: Signal65/next.js-CodeRabbit

Length of output: 691


🏁 Script executed:

# Check if there are any version checks or other guards in the file
rg -n "React\." packages/next/src/server/node-environment-extensions/console-dim.external.tsx | head -20

Repository: Signal65/next.js-CodeRabbit

Length of output: 179


Guard cacheSignal method call for React builds that don't implement it.

cacheSignal is only available as a stable API in React 19.2.0 (October 2025). Since Next.js must support older React versions (18.x and earlier 19.x releases), the code will throw when getClientReact() or getServerReact() return objects without the cacheSignal method. This prevents the fallback to workUnitStore from ever executing.

Use optional chaining on the method call itself:

🛠️ Required fix
-      const signal =
-        getClientReact()?.cacheSignal() ?? getServerReact()?.cacheSignal()
+      const signal =
+        getClientReact()?.cacheSignal?.() ?? getServerReact()?.cacheSignal?.()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const signal =
getClientReact()?.cacheSignal() ?? getServerReact()?.cacheSignal()
if (signal) {
// We are in a React Server render and can consult the React cache signal to determine if logs
// are now dimmable.
if (signal.aborted) {
if (currentAbortedLogsStyle === HIDDEN_STYLE) {
return
}
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else if (consoleStore?.dim === true) {
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else {
return originalMethod.apply(this, args)
}
const signal =
getClientReact()?.cacheSignal?.() ?? getServerReact()?.cacheSignal?.()
if (signal) {
// We are in a React Server render and can consult the React cache signal to determine if logs
// are now dimmable.
if (signal.aborted) {
if (currentAbortedLogsStyle === HIDDEN_STYLE) {
return
}
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else if (consoleStore?.dim === true) {
return applyWithDimming.call(
this,
consoleStore,
originalMethod,
methodName,
args
)
} else {
return originalMethod.apply(this, args)
}
🤖 Prompt for AI Agents
In
`@packages/next/src/server/node-environment-extensions/console-dim.external.tsx`
around lines 189 - 215, The code calls cacheSignal() unguarded which will throw
for React builds that don't implement it; update the signal assignment to call
cacheSignal via optional chaining on the method call (e.g.,
getClientReact()?.cacheSignal?.() ?? getServerReact()?.cacheSignal?.()) so that
when getClientReact() or getServerReact() lack cacheSignal the code falls back
to the existing workUnitStore logic; ensure this change preserves the subsequent
use of signal and does not alter the applyWithDimming/originalMethod branches
(symbols: getClientReact, getServerReact, cacheSignal, signal, applyWithDimming,
originalMethod, methodName, args, consoleStore).

}

Expand Down
22 changes: 10 additions & 12 deletions packages/next/src/server/node-environment-extensions/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { workAsyncStorage } from '../app-render/work-async-storage.external'
import {
workUnitAsyncStorage,
type PrerenderStoreModern,
} from '../app-render/work-unit-async-storage.external'
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'
import {
abortOnSynchronousPlatformIOAccess,
trackSynchronousPlatformIOAccessInDev,
} from '../app-render/dynamic-rendering'
import { InvariantError } from '../../shared/lib/invariant-error'

import { getServerReact, getClientReact } from '../runtime-reacts.external'

type ApiType = 'time' | 'random' | 'crypto'

export function io(expression: string, type: ApiType) {
Expand Down Expand Up @@ -47,7 +46,7 @@ export function io(expression: string, type: ApiType) {
abortOnSynchronousPlatformIOAccess(
workStore.route,
expression,
applyOwnerStack(new Error(message), workUnitStore),
applyOwnerStack(new Error(message)),
workUnitStore
)
}
Expand Down Expand Up @@ -79,7 +78,7 @@ export function io(expression: string, type: ApiType) {
abortOnSynchronousPlatformIOAccess(
workStore.route,
expression,
applyOwnerStack(new Error(message), workUnitStore),
applyOwnerStack(new Error(message)),
workUnitStore
)
}
Expand All @@ -101,16 +100,15 @@ export function io(expression: string, type: ApiType) {
}
}

function applyOwnerStack(error: Error, workUnitStore: PrerenderStoreModern) {
function applyOwnerStack(error: Error) {
// TODO: Instead of stitching the stacks here, we should log the original
// error as-is when it occurs, and let `patchErrorInspect` handle adding the
// owner stack, instead of logging it deferred in the `LogSafely` component
// via `throwIfDisallowedDynamic`.
if (
process.env.NODE_ENV !== 'production' &&
workUnitStore.captureOwnerStack
) {
const ownerStack = workUnitStore.captureOwnerStack()
if (process.env.NODE_ENV !== 'production') {
const ownerStack =
getClientReact()?.captureOwnerStack() ??
getServerReact()?.captureOwnerStack()

if (ownerStack) {
let stack = ownerStack
Expand Down
13 changes: 6 additions & 7 deletions packages/next/src/server/route-modules/app-page/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ if (process.env.NEXT_RUNTIME !== 'edge') {
vendoredReactSSR =
require('./vendored/ssr/entrypoints') as typeof import('./vendored/ssr/entrypoints')

// In Node environments we augment console logging with information contextual to a React render.
// This patching is global so we need to register the cacheSignal getter from our bundled React instances
// here when we load them rather than in the external module itself when the patch is applied.
const { registerGetCacheSignal } =
require('../../node-environment-extensions/console-dim.external') as typeof import('../../node-environment-extensions/console-dim.external')
registerGetCacheSignal(vendoredReactRSC.React.cacheSignal)
registerGetCacheSignal(vendoredReactSSR.React.cacheSignal)
// In Node environments we need to access the correct React instance from external modules such
// as global patches. We register the loaded React instances here.
const { registerServerReact, registerClientReact } =
require('../../runtime-reacts.external') as typeof import('../../runtime-reacts.external')
registerServerReact(vendoredReactRSC.React)
registerClientReact(vendoredReactSSR.React)
}

/**
Expand Down
2 changes: 0 additions & 2 deletions packages/next/src/server/route-modules/app-route/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,6 @@ export class AppRouteRouteModule extends RouteModule<
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash: undefined,
captureOwnerStack: undefined,
})

let prospectiveResult
Expand Down Expand Up @@ -505,7 +504,6 @@ export class AppRouteRouteModule extends RouteModule<
prerenderResumeDataCache,
renderResumeDataCache: null,
hmrRefreshHash: undefined,
captureOwnerStack: undefined,
})

let responseHandled = false
Expand Down
15 changes: 15 additions & 0 deletions packages/next/src/server/runtime-reacts.external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
let ClientReact: typeof import('react') | null = null
export function registerClientReact(react: typeof import('react')) {
ClientReact = react
}
export function getClientReact() {
return ClientReact
}

let ServerReact: typeof import('react') | null = null
export function registerServerReact(react: typeof import('react')) {
ServerReact = react
}
export function getServerReact() {
return ServerReact
}
1 change: 1 addition & 0 deletions test/production/next-server-nft/next-server-nft.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ const isReact18 = parseInt(process.env.NEXT_TEST_REACT_VERSION) === 18
"/node_modules/next/dist/server/route-modules/pages/vendored/contexts/loadable.js",
"/node_modules/next/dist/server/route-modules/pages/vendored/contexts/router-context.js",
"/node_modules/next/dist/server/route-modules/pages/vendored/contexts/server-inserted-html.js",
"/node_modules/next/dist/server/runtime-reacts.external.js",
"/node_modules/next/dist/shared/lib/deep-freeze.js",
"/node_modules/next/dist/shared/lib/invariant-error.js",
"/node_modules/next/dist/shared/lib/is-plain-object.js",
Expand Down