Skip to content

Commit b367b60

Browse files
authored
[Flight] Add "use ..." boundary after the change instead of before it (#33478)
I noticed that the ThirdPartyComponent in the fixture was showing the wrong stack and the `"use third-party"` is in the wrong location. <img width="628" alt="Screenshot 2025-06-06 at 11 22 11 PM" src="https://github.com/user-attachments/assets/f0013380-d79e-4765-b371-87fd61b3056b" /> When creating the initial JSX inside the third party server, we should make sure that it has no owner. In a real cross-server environment you get this by default by just executing in different context. But since the fixture example is inside the same AsyncLocalStorage as the parent it already has an owner which gets transferred. So we should make sure that were we create the JSX has no owner to simulate this. When we then parse a null owner on the receiving side, we replace its owner/stack with the owner/stack of the call to `createFrom...` to connect them. This worked fine with only two environments. The bug was that when we did this and then transferred the result to a third environment we took the original parsed stack trace. We should instead parse a new one from the replaced stack in the current environment. The second bug was that the `"use third-party"` badge ends up in the wrong place when we do this kind of thing. Because the stack of the thing entering the new environment is the call to `createFrom...` which is in the old environment even though the component itself executes in the new environment. So to see if there's a change we should be comparing the current environment of the task to the owner's environment instead of the next environment after the task. After: <img width="494" alt="Screenshot 2025-06-07 at 1 13 28 AM" src="https://github.com/user-attachments/assets/e2e870ba-f125-4526-a853-bd29f164cf09" />
1 parent 9666605 commit b367b60

File tree

7 files changed

+152
-138
lines changed

7 files changed

+152
-138
lines changed

fixtures/flight/src/App.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,26 @@ function Foo({children}) {
3333
return <div>{children}</div>;
3434
}
3535

36+
async function delay(text, ms) {
37+
return new Promise(resolve => setTimeout(() => resolve(text), ms));
38+
}
39+
3640
async function Bar({children}) {
37-
await new Promise(resolve => setTimeout(() => resolve('deferred text'), 10));
41+
await delay('deferred text', 10);
3842
return <div>{children}</div>;
3943
}
4044

4145
async function ThirdPartyComponent() {
42-
return new Promise(resolve =>
43-
setTimeout(() => resolve('hello from a 3rd party'), 30)
44-
);
46+
return delay('hello from a 3rd party', 30);
4547
}
4648

4749
// Using Web streams for tee'ing convenience here.
4850
let cachedThirdPartyReadableWeb;
4951

52+
// We create the Component outside of AsyncLocalStorage so that it has no owner.
53+
// That way it gets the owner from the call to createFromNodeStream.
54+
const thirdPartyComponent = <ThirdPartyComponent />;
55+
5056
function fetchThirdParty(noCache) {
5157
if (cachedThirdPartyReadableWeb && !noCache) {
5258
const [readableWeb1, readableWeb2] = cachedThirdPartyReadableWeb.tee();
@@ -59,7 +65,7 @@ function fetchThirdParty(noCache) {
5965
}
6066

6167
const stream = renderToPipeableStream(
62-
<ThirdPartyComponent />,
68+
thirdPartyComponent,
6369
{},
6470
{environmentName: 'third-party'}
6571
);
@@ -80,8 +86,8 @@ function fetchThirdParty(noCache) {
8086
}
8187

8288
async function ServerComponent({noCache}) {
83-
await new Promise(resolve => setTimeout(() => resolve('deferred text'), 50));
84-
return fetchThirdParty(noCache);
89+
await delay('deferred text', 50);
90+
return await fetchThirdParty(noCache);
8591
}
8692

8793
export default async function App({prerender, noCache}) {

packages/react-client/src/ReactFlightClient.js

Lines changed: 41 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@ function createElement(
844844
// This owner should ideally have already been initialized to avoid getting
845845
// user stack frames on the stack.
846846
const ownerTask =
847-
owner === null ? null : initializeFakeTask(response, owner, env);
847+
owner === null ? null : initializeFakeTask(response, owner);
848848
if (ownerTask === null) {
849849
const rootTask = response._debugRootTask;
850850
if (rootTask != null) {
@@ -2494,7 +2494,6 @@ function getRootTask(
24942494
function initializeFakeTask(
24952495
response: Response,
24962496
debugInfo: ReactComponentInfo | ReactAsyncInfo | ReactIOInfo,
2497-
childEnvironmentName: string,
24982497
): null | ConsoleTask {
24992498
if (!supportsCreateTask) {
25002499
return null;
@@ -2504,6 +2503,10 @@ function initializeFakeTask(
25042503
// If it's null, we can't initialize a task.
25052504
return null;
25062505
}
2506+
const cachedEntry = debugInfo.debugTask;
2507+
if (cachedEntry !== undefined) {
2508+
return cachedEntry;
2509+
}
25072510

25082511
// Workaround for a bug where Chrome Performance tracking uses the enclosing line/column
25092512
// instead of the callsite. For ReactAsyncInfo/ReactIOInfo, the only thing we're going
@@ -2516,47 +2519,35 @@ function initializeFakeTask(
25162519
const stack = debugInfo.stack;
25172520
const env: string =
25182521
debugInfo.env == null ? response._rootEnvironmentName : debugInfo.env;
2519-
if (env !== childEnvironmentName) {
2522+
const ownerEnv: string =
2523+
debugInfo.owner == null || debugInfo.owner.env == null
2524+
? response._rootEnvironmentName
2525+
: debugInfo.owner.env;
2526+
const ownerTask =
2527+
debugInfo.owner == null
2528+
? null
2529+
: initializeFakeTask(response, debugInfo.owner);
2530+
const taskName =
25202531
// This is the boundary between two environments so we'll annotate the task name.
2521-
// That is unusual so we don't cache it.
2522-
const ownerTask =
2523-
debugInfo.owner == null
2524-
? null
2525-
: initializeFakeTask(response, debugInfo.owner, env);
2526-
return buildFakeTask(
2527-
response,
2528-
ownerTask,
2529-
stack,
2530-
'"use ' + childEnvironmentName.toLowerCase() + '"',
2531-
env,
2532-
useEnclosingLine,
2533-
);
2534-
} else {
2535-
const cachedEntry = debugInfo.debugTask;
2536-
if (cachedEntry !== undefined) {
2537-
return cachedEntry;
2538-
}
2539-
const ownerTask =
2540-
debugInfo.owner == null
2541-
? null
2542-
: initializeFakeTask(response, debugInfo.owner, env);
2543-
// Some unfortunate pattern matching to refine the type.
2544-
const taskName =
2545-
debugInfo.key !== undefined
2532+
// We assume that the stack frame of the entry into the new environment was done
2533+
// from the old environment. So we use the owner's environment as the current.
2534+
env !== ownerEnv
2535+
? '"use ' + env.toLowerCase() + '"'
2536+
: // Some unfortunate pattern matching to refine the type.
2537+
debugInfo.key !== undefined
25462538
? getServerComponentTaskName(((debugInfo: any): ReactComponentInfo))
25472539
: debugInfo.name !== undefined
25482540
? getIOInfoTaskName(((debugInfo: any): ReactIOInfo))
25492541
: getAsyncInfoTaskName(((debugInfo: any): ReactAsyncInfo));
2550-
// $FlowFixMe[cannot-write]: We consider this part of initialization.
2551-
return (debugInfo.debugTask = buildFakeTask(
2552-
response,
2553-
ownerTask,
2554-
stack,
2555-
taskName,
2556-
env,
2557-
useEnclosingLine,
2558-
));
2559-
}
2542+
// $FlowFixMe[cannot-write]: We consider this part of initialization.
2543+
return (debugInfo.debugTask = buildFakeTask(
2544+
response,
2545+
ownerTask,
2546+
stack,
2547+
taskName,
2548+
ownerEnv,
2549+
useEnclosingLine,
2550+
));
25602551
}
25612552

25622553
function buildFakeTask(
@@ -2658,27 +2649,30 @@ function resolveDebugInfo(
26582649
'resolveDebugInfo should never be called in production mode. This is a bug in React.',
26592650
);
26602651
}
2661-
// We eagerly initialize the fake task because this resolving happens outside any
2662-
// render phase so we're not inside a user space stack at this point. If we waited
2663-
// to initialize it when we need it, we might be inside user code.
2664-
const env =
2665-
debugInfo.env === undefined ? response._rootEnvironmentName : debugInfo.env;
26662652
if (debugInfo.stack !== undefined) {
26672653
const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
26682654
// $FlowFixMe[incompatible-type]
26692655
debugInfo;
2670-
initializeFakeTask(response, componentInfoOrAsyncInfo, env);
2656+
// We eagerly initialize the fake task because this resolving happens outside any
2657+
// render phase so we're not inside a user space stack at this point. If we waited
2658+
// to initialize it when we need it, we might be inside user code.
2659+
initializeFakeTask(response, componentInfoOrAsyncInfo);
26712660
}
2672-
if (debugInfo.owner === null && response._debugRootOwner != null) {
2661+
if (debugInfo.owner == null && response._debugRootOwner != null) {
26732662
const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
26742663
// $FlowFixMe: By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo`
26752664
debugInfo;
26762665
// $FlowFixMe[cannot-write]
26772666
componentInfoOrAsyncInfo.owner = response._debugRootOwner;
2667+
// We clear the parsed stack frames to indicate that it needs to be re-parsed from debugStack.
2668+
// $FlowFixMe[cannot-write]
2669+
componentInfoOrAsyncInfo.stack = null;
26782670
// We override the stack if we override the owner since the stack where the root JSX
26792671
// was created on the server isn't very useful but where the request was made is.
26802672
// $FlowFixMe[cannot-write]
26812673
componentInfoOrAsyncInfo.debugStack = response._debugRootStack;
2674+
// $FlowFixMe[cannot-write]
2675+
componentInfoOrAsyncInfo.debugTask = response._debugRootTask;
26822676
} else if (debugInfo.stack !== undefined) {
26832677
const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
26842678
// $FlowFixMe[incompatible-type]
@@ -2738,7 +2732,7 @@ const replayConsoleWithCallStack = {
27382732
bindToConsole(methodName, args, env),
27392733
);
27402734
if (owner != null) {
2741-
const task = initializeFakeTask(response, owner, env);
2735+
const task = initializeFakeTask(response, owner);
27422736
initializeFakeStack(response, owner);
27432737
if (task !== null) {
27442738
task.run(callStack);
@@ -2812,10 +2806,8 @@ function resolveConsoleEntry(
28122806
}
28132807

28142808
function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
2815-
const env =
2816-
ioInfo.env === undefined ? response._rootEnvironmentName : ioInfo.env;
28172809
if (ioInfo.stack !== undefined) {
2818-
initializeFakeTask(response, ioInfo, env);
2810+
initializeFakeTask(response, ioInfo);
28192811
initializeFakeStack(response, ioInfo);
28202812
}
28212813
// Adjust the time to the current environment's time space.

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ describe('ReactFlight', () => {
320320
name: 'Greeting',
321321
env: 'Server',
322322
key: null,
323-
owner: null,
324323
stack: ' in Object.<anonymous> (at **)',
325324
props: {
326325
firstName: 'Seb',
@@ -364,7 +363,6 @@ describe('ReactFlight', () => {
364363
name: 'Greeting',
365364
env: 'Server',
366365
key: null,
367-
owner: null,
368366
stack: ' in Object.<anonymous> (at **)',
369367
props: {
370368
firstName: 'Seb',
@@ -2812,7 +2810,6 @@ describe('ReactFlight', () => {
28122810
name: 'ServerComponent',
28132811
env: 'Server',
28142812
key: null,
2815-
owner: null,
28162813
stack: ' in Object.<anonymous> (at **)',
28172814
props: {
28182815
transport: expect.arrayContaining([]),
@@ -2834,7 +2831,6 @@ describe('ReactFlight', () => {
28342831
name: 'ThirdPartyComponent',
28352832
env: 'third-party',
28362833
key: null,
2837-
owner: null,
28382834
stack: ' in Object.<anonymous> (at **)',
28392835
props: {},
28402836
},
@@ -2851,7 +2847,6 @@ describe('ReactFlight', () => {
28512847
name: 'ThirdPartyLazyComponent',
28522848
env: 'third-party',
28532849
key: null,
2854-
owner: null,
28552850
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
28562851
props: {},
28572852
},
@@ -2867,7 +2862,6 @@ describe('ReactFlight', () => {
28672862
name: 'ThirdPartyFragmentComponent',
28682863
env: 'third-party',
28692864
key: '3',
2870-
owner: null,
28712865
stack: ' in Object.<anonymous> (at **)',
28722866
props: {},
28732867
},
@@ -2941,7 +2935,6 @@ describe('ReactFlight', () => {
29412935
name: 'ServerComponent',
29422936
env: 'Server',
29432937
key: null,
2944-
owner: null,
29452938
stack: ' in Object.<anonymous> (at **)',
29462939
props: {
29472940
transport: expect.arrayContaining([]),
@@ -2961,7 +2954,6 @@ describe('ReactFlight', () => {
29612954
name: 'Keyed',
29622955
env: 'Server',
29632956
key: 'keyed',
2964-
owner: null,
29652957
stack: ' in ServerComponent (at **)',
29662958
props: {
29672959
children: {},
@@ -2980,7 +2972,6 @@ describe('ReactFlight', () => {
29802972
name: 'ThirdPartyAsyncIterableComponent',
29812973
env: 'third-party',
29822974
key: null,
2983-
owner: null,
29842975
stack: ' in Object.<anonymous> (at **)',
29852976
props: {},
29862977
},
@@ -3137,7 +3128,6 @@ describe('ReactFlight', () => {
31373128
name: 'Component',
31383129
env: 'A',
31393130
key: null,
3140-
owner: null,
31413131
stack: ' in Object.<anonymous> (at **)',
31423132
props: {},
31433133
},
@@ -3325,7 +3315,6 @@ describe('ReactFlight', () => {
33253315
name: 'Greeting',
33263316
env: 'Server',
33273317
key: null,
3328-
owner: null,
33293318
stack: ' in Object.<anonymous> (at **)',
33303319
props: {
33313320
firstName: 'Seb',

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,6 @@ describe('ReactFlightDOMBrowser', () => {
708708
name: 'Server',
709709
env: 'Server',
710710
key: null,
711-
owner: null,
712711
}),
713712
}),
714713
);
@@ -724,7 +723,6 @@ describe('ReactFlightDOMBrowser', () => {
724723
name: 'Server',
725724
env: 'Server',
726725
key: null,
727-
owner: null,
728726
}),
729727
}),
730728
);

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,6 @@ describe('ReactFlightDOMEdge', () => {
11901190
const greetInfo = expect.objectContaining({
11911191
name: 'Greeting',
11921192
env: 'Server',
1193-
owner: null,
11941193
});
11951194
expect(lazyWrapper._debugInfo).toEqual([
11961195
{time: 12},

0 commit comments

Comments
 (0)