From 3d14fcf03f4e296d21b52b362f5adefd9e366375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 24 Jul 2025 11:07:11 -0400 Subject: [PATCH 1/2] [Flight] Use about: protocol instead of rsc: protocol for fake evals (#33977) Chrome DevTools Extensions has a silly problem where they block access to load Resources from all protocols except [an allow list](https://github.com/ChromeDevTools/devtools-frontend/blob/eb970fbc6482f281b95bbec1c33c1c539f6d50f0/front_end/models/extensions/ExtensionServer.ts#L60). https://issues.chromium.org/issues/416196401 Even though these are `eval()` and not actually loaded from the network they're blocked. They can really be any string. We just have to pick one of: ```js 'http:', 'https:', 'file:', 'data:', 'chrome-extension:', 'about:' ``` That way React DevTools extensions can load this content to source map them. Webpack has the same issue with its `webpack://` and `webpack-internal://` urls. --- packages/react-client/src/ReactFlightClient.js | 4 ++-- packages/react-client/src/ReactFlightReplyClient.js | 4 ++-- packages/react-client/src/__tests__/ReactFlight-test.js | 6 +++--- packages/react-server/src/ReactFlightServer.js | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 5281c20966a81..4403687383ae7 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -3154,7 +3154,7 @@ function createFakeFunction( } if (sourceMap) { - // We use the prefix rsc://React/ to separate these from other files listed in + // We use the prefix about://React/ to separate these from other files listed in // the Chrome DevTools. We need a "host name" and not just a protocol because // otherwise the group name becomes the root folder. Ideally we don't want to // show these at all but there's two reasons to assign a fake URL. @@ -3162,7 +3162,7 @@ function createFakeFunction( // 2) If source maps are disabled or fails, you should at least be able to tell // which file it was. code += - '\n//# sourceURL=rsc://React/' + + '\n//# sourceURL=about://React/' + encodeURIComponent(environmentName) + '/' + encodeURI(filename) + diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index d22a894b5157c..22479aac04471 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -1095,7 +1095,7 @@ function createFakeServerFunction, T>( } if (sourceMap) { - // We use the prefix rsc://React/ to separate these from other files listed in + // We use the prefix about://React/ to separate these from other files listed in // the Chrome DevTools. We need a "host name" and not just a protocol because // otherwise the group name becomes the root folder. Ideally we don't want to // show these at all but there's two reasons to assign a fake URL. @@ -1103,7 +1103,7 @@ function createFakeServerFunction, T>( // 2) If source maps are disabled or fails, you should at least be able to tell // which file it was. code += - '\n//# sourceURL=rsc://React/' + + '\n//# sourceURL=about://React/' + encodeURIComponent(environmentName) + '/' + encodeURI(filename) + diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 6c1b22f1414ed..2ccf874cee91b 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1314,7 +1314,7 @@ describe('ReactFlight', () => { ' at async file:///testing.js:42:3', // third-party RSC frame // Ideally this would be a real frame produced by React not a mocked one. - ' at ThirdParty (rsc://React/ThirdParty/file:///code/%5Broot%2520of%2520the%2520server%5D.js?42:1:1)', + ' at ThirdParty (about://React/ThirdParty/file:///code/%5Broot%2520of%2520the%2520server%5D.js?42:1:1)', // We'll later filter this out based on line/column in `filterStackFrame`. ' at ThirdPartyModule (file:///file-with-index-source-map.js:52656:16374)', // host component in parent stack @@ -3073,7 +3073,7 @@ describe('ReactFlight', () => { ReactNoopFlightClient.read(transport, { findSourceMapURL(url) { // By giving a source map url we're saying that we can't use the original - // file as the sourceURL, which gives stack traces a rsc://React/ prefix. + // file as the sourceURL, which gives stack traces a about://React/ prefix. return 'source-map://' + url; }, }), @@ -3147,7 +3147,7 @@ describe('ReactFlight', () => { expectedErrorStack={expectedErrorStack}> {ReactNoopFlightClient.read(transport, { findSourceMapURL(url, environmentName) { - if (url.startsWith('rsc://React/')) { + if (url.startsWith('about://React/')) { // We don't expect to see any React prefixed URLs here. sawReactPrefix = true; } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 2ba4c0dee8277..688a9ef0c574a 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -179,12 +179,12 @@ function defaultFilterStackFrame( } function devirtualizeURL(url: string): string { - if (url.startsWith('rsc://React/')) { + if (url.startsWith('about://React/')) { // This callsite is a virtual fake callsite that came from another Flight client. // We need to reverse it back into the original location by stripping its prefix // and suffix. We don't need the environment name because it's available on the // parent object that will contain the stack. - const envIdx = url.indexOf('/', 'rsc://React/'.length); + const envIdx = url.indexOf('/', 'about://React/'.length); const suffixIdx = url.lastIndexOf('?'); if (envIdx > -1 && suffixIdx > -1) { return decodeURI(url.slice(envIdx + 1, suffixIdx)); From 4f34cc4a2e1198493375867d1876509ae9771aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 24 Jul 2025 13:33:03 -0400 Subject: [PATCH 2/2] [Fiber] Don't throw away the Error object retaining the owner stack (#33976) We currently throw away the Error once we've used to the owner stack of a Fiber once. This maybe helps a bit with memory and redoing it but we really don't expect most Fibers to hit this at all. It's not very hot. If we throw away the Error, then we can't use native debugger protocols to inspect the native stack. Instead, we'd have to maintain a url to resource map indefinitely like what Chrome DevTools does to map a url to a resource. Technically it's not even technically correct since the file path might not be reversible and could in theory conflict. --- .../src/backend/fiber/DevToolsFiberComponentStack.js | 2 +- .../react-reconciler/src/ReactFiberComponentStack.js | 11 ++++------- packages/react-reconciler/src/ReactInternalTypes.js | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js index 4f63f562e0b51..c938b6736323e 100644 --- a/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js +++ b/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js @@ -199,7 +199,7 @@ export function getOwnerStackByFiberInDev( if (typeof owner.tag === 'number') { const fiber: Fiber = (owner: any); owner = fiber._debugOwner; - let debugStack = fiber._debugStack; + let debugStack: void | null | string | Error = fiber._debugStack; // If we don't actually print the stack if there is no owner of this JSX element. // In a real app it's typically not useful since the root app is always controlled // by the framework. These also tend to have noisy stacks because they're not rooted diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js index 80fc21a9e64de..da804171da5b1 100644 --- a/packages/react-reconciler/src/ReactFiberComponentStack.js +++ b/packages/react-reconciler/src/ReactFiberComponentStack.js @@ -177,7 +177,7 @@ export function getOwnerStackByFiberInDev(workInProgress: Fiber): string { if (typeof owner.tag === 'number') { const fiber: Fiber = (owner: any); owner = fiber._debugOwner; - let debugStack = fiber._debugStack; + const debugStack = fiber._debugStack; // If we don't actually print the stack if there is no owner of this JSX element. // In a real app it's typically not useful since the root app is always controlled // by the framework. These also tend to have noisy stacks because they're not rooted @@ -185,12 +185,9 @@ export function getOwnerStackByFiberInDev(workInProgress: Fiber): string { // if the element was created in module scope. E.g. hoisted. We could add a a single // stack frame for context for example but it doesn't say much if that's a wrapper. if (owner && debugStack) { - if (typeof debugStack !== 'string') { - // Stash the formatted stack so that we can avoid redoing the filtering. - fiber._debugStack = debugStack = formatOwnerStack(debugStack); - } - if (debugStack !== '') { - info += '\n' + debugStack; + const formattedStack = formatOwnerStack(debugStack); + if (formattedStack !== '') { + info += '\n' + formattedStack; } } } else if (owner.debugStack != null) { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index ec75513892900..b8d66015fc772 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -200,7 +200,7 @@ export type Fiber = { _debugInfo?: ReactDebugInfo | null, _debugOwner?: ReactComponentInfo | Fiber | null, - _debugStack?: string | Error | null, + _debugStack?: Error | null, _debugTask?: ConsoleTask | null, _debugNeedsRemount?: boolean,