Skip to content

Commit

Permalink
Reverse engineer original stack frames when virtual frames are re-ser…
Browse files Browse the repository at this point in the history
…ialized
  • Loading branch information
sebmarkbage committed Jul 22, 2024
1 parent 1b805b9 commit c889895
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 2 deletions.
104 changes: 103 additions & 1 deletion packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,14 @@ describe('ReactFlight', () => {
this.props.expectedMessage,
);
expect(this.state.error.digest).toBe('a dev digest');
expect(this.state.error.environmentName).toBe('Server');
expect(this.state.error.environmentName).toBe(
this.props.expectedEnviromentName || 'Server',
);
if (this.props.expectedErrorStack !== undefined) {
expect(this.state.error.stack).toContain(
this.props.expectedErrorStack,
);
}
} else {
expect(this.state.error.message).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production' +
Expand Down Expand Up @@ -2603,6 +2610,101 @@ describe('ReactFlight', () => {
);
});

it('preserves error stacks passed through server-to-server with source maps', async () => {
async function ServerComponent({transport}) {
// This is a Server Component that receives other Server Components from a third party.
const thirdParty = ReactServer.use(
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.
return 'source-map://' + url;
},
}),
);
// This will throw a third-party error inside the first-party server component.
await thirdParty.model;
return 'Should never render';
}

async function bar() {
throw new Error('third-party-error');
}

async function foo() {
await bar();
}

const rejectedPromise = foo();

const thirdPartyTransport = ReactNoopFlightServer.render(
{model: rejectedPromise},
{
environmentName: 'third-party',
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
return `digest("${x.message}")`;
},
},
);

let originalError;
try {
await rejectedPromise;
} catch (x) {
originalError = x;
}
expect(originalError.message).toBe('third-party-error');

const transport = ReactNoopFlightServer.render(
<ServerComponent transport={thirdPartyTransport} />,
{
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
return x.digest; // passthrough
},
},
);

await 0;
await 0;
await 0;

const expectedErrorStack = originalError.stack
// Test only the first rows since there's a lot of noise after that is eliminated.
.split('\n')
.slice(0, 4)
.join('\n')
.replaceAll(' (/', ' (file:///'); // The eval will end up normalizing these

let sawReactPrefix = false;
await act(async () => {
ReactNoop.render(
<ErrorBoundary
expectedMessage="third-party-error"
expectedEnviromentName="third-party"
expectedErrorStack={expectedErrorStack}>
{ReactNoopFlightClient.read(transport, {
findSourceMapURL(url) {
if (url.startsWith('rsc://React/')) {
// We don't expect to see any React prefixed URLs here.
sawReactPrefix = true;
}
// My not giving a source map, we should leave it intact.
return null;
},
})}
</ErrorBoundary>,
);
});

expect(sawReactPrefix).toBe(false);
});

it('can change the environment name inside a component', async () => {
let env = 'A';
function Component(props) {
Expand Down
16 changes: 15 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,21 @@ function filterStackTrace(error: Error, skipFrames: number): ReactStackTrace {
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
// the DevTools or framework's ignore lists to filter them out.
return parseStackTrace(error, skipFrames).filter(isNotExternal);
const stack = parseStackTrace(error, skipFrames).filter(isNotExternal);
for (let i = 0; i < stack.length; i++) {
const callsite = stack[i];
const url = callsite[1];
if (url.startsWith('rsc://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.
const suffixIdx = url.lastIndexOf('?');
if (suffixIdx > -1) {
callsite[1] = url.slice(12, suffixIdx);
}
}
}
return stack;
}

initAsyncDebugInfo();
Expand Down

0 comments on commit c889895

Please sign in to comment.