Skip to content

Commit da7487b

Browse files
authored
[Flight] Skip the stack frame of built-in wrappers that create or await Promises (facebook#33798)
We already do this with `"new Promise"` and `"Promise.then"`. There are also many helpers that both create promises and awaits other promises inside of it like `Promise.all`. The way this is filtered is different from just filtering out all anonymous stacks since they're used to determine where the boundary is between ignore listed and user space. Ideally we'd cover more wrappers that are internal to Promise libraries.
1 parent 9fec565 commit da7487b

File tree

2 files changed

+568
-284
lines changed

2 files changed

+568
-284
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,49 @@ function devirtualizeURL(url: string): string {
194194
return url;
195195
}
196196

197+
function isPromiseCreationInternal(url: string, functionName: string): boolean {
198+
// Various internals of the JS VM can create Promises but the call frame of the
199+
// internals are not very interesting for our purposes so we need to skip those.
200+
if (url === 'node:internal/async_hooks') {
201+
// Ignore the stack frames from the async hooks themselves.
202+
return true;
203+
}
204+
if (url !== '') {
205+
return false;
206+
}
207+
switch (functionName) {
208+
case 'new Promise':
209+
case 'Function.withResolvers':
210+
case 'Function.reject':
211+
case 'Function.resolve':
212+
case 'Function.all':
213+
case 'Function.allSettled':
214+
case 'Function.race':
215+
case 'Function.try':
216+
return true;
217+
default:
218+
return false;
219+
}
220+
}
221+
222+
function stripLeadingPromiseCreationFrames(
223+
stack: ReactStackTrace,
224+
): ReactStackTrace {
225+
for (let i = 0; i < stack.length; i++) {
226+
const callsite = stack[i];
227+
const functionName = callsite[0];
228+
const url = callsite[1];
229+
if (!isPromiseCreationInternal(url, functionName)) {
230+
if (i > 0) {
231+
return stack.slice(i);
232+
} else {
233+
return stack;
234+
}
235+
}
236+
}
237+
return [];
238+
}
239+
197240
function findCalledFunctionNameFromStackTrace(
198241
request: Request,
199242
stack: ReactStackTrace,
@@ -207,11 +250,7 @@ function findCalledFunctionNameFromStackTrace(
207250
const url = devirtualizeURL(callsite[1]);
208251
const lineNumber = callsite[2];
209252
const columnNumber = callsite[3];
210-
if (functionName === 'new Promise') {
211-
// Ignore Promise constructors.
212-
} else if (url === 'node:internal/async_hooks') {
213-
// Ignore the stack frames from the async hooks themselves.
214-
} else if (filterStackFrame(url, functionName, lineNumber, columnNumber)) {
253+
if (filterStackFrame(url, functionName, lineNumber, columnNumber)) {
215254
if (bestMatch === '') {
216255
// If we had no good stack frames for internal calls, just use the last
217256
// first party function name.
@@ -275,13 +314,44 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
275314
return false;
276315
}
277316

317+
function isPromiseAwaitInternal(url: string, functionName: string): boolean {
318+
// Various internals of the JS VM can await internally on a Promise. If those are at
319+
// the top of the stack then we don't want to consider them as internal frames. The
320+
// true "await" conceptually is the thing that called the helper.
321+
// Ideally we'd also include common third party helpers for this.
322+
if (url === 'node:internal/async_hooks') {
323+
// Ignore the stack frames from the async hooks themselves.
324+
return true;
325+
}
326+
if (url !== '') {
327+
return false;
328+
}
329+
switch (functionName) {
330+
case 'Promise.then':
331+
case 'Promise.catch':
332+
case 'Promise.finally':
333+
case 'Function.reject':
334+
case 'Function.resolve':
335+
case 'Function.all':
336+
case 'Function.allSettled':
337+
case 'Function.race':
338+
case 'Function.try':
339+
return true;
340+
default:
341+
return false;
342+
}
343+
}
344+
278345
export function isAwaitInUserspace(
279346
request: Request,
280347
stack: ReactStackTrace,
281348
): boolean {
282349
let firstFrame = 0;
283-
while (stack.length > firstFrame && stack[firstFrame][0] === 'Promise.then') {
284-
// Skip Promise.then frame itself.
350+
while (
351+
stack.length > firstFrame &&
352+
isPromiseAwaitInternal(stack[firstFrame][1], stack[firstFrame][0])
353+
) {
354+
// Skip the internal frame that awaits itself.
285355
firstFrame++;
286356
}
287357
if (stack.length > firstFrame) {
@@ -4213,7 +4283,8 @@ function serializeIONode(
42134283
let stack = null;
42144284
let name = '';
42154285
if (ioNode.stack !== null) {
4216-
const fullStack = ioNode.stack;
4286+
// The stack can contain some leading internal frames for the construction of the promise that we skip.
4287+
const fullStack = stripLeadingPromiseCreationFrames(ioNode.stack);
42174288
stack = filterStackTrace(request, fullStack);
42184289
name = findCalledFunctionNameFromStackTrace(request, fullStack);
42194290
// The name can include the object that this was called on but sometimes that's

0 commit comments

Comments
 (0)