mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
309e146193
Stacked on #30132. This way we can get parent and owner stacks from the error. This forces us to confront multiple errors and whether or not a Flight error that ends up being unobservable needs to really reject the render. This implements stashing of Flight errors with a digest and only errors if they end up erroring the Fizz render too. At this point they'll have parent stacks so we can surface those.
234 lines
6.9 KiB
JavaScript
234 lines
6.9 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {ReactNodeList} from 'shared/ReactTypes';
|
|
import type {LazyComponent} from 'react/src/ReactLazy';
|
|
import type {ErrorInfo} from 'react-server/src/ReactFizzServer';
|
|
|
|
import ReactVersion from 'shared/ReactVersion';
|
|
|
|
import ReactSharedInternalsServer from 'react-server/src/ReactSharedInternalsServer';
|
|
import ReactSharedInternalsClient from 'shared/ReactSharedInternals';
|
|
|
|
import {
|
|
createRequest as createFlightRequest,
|
|
startWork as startFlightWork,
|
|
startFlowing as startFlightFlowing,
|
|
abort as abortFlight,
|
|
} from 'react-server/src/ReactFlightServer';
|
|
|
|
import {
|
|
createResponse as createFlightResponse,
|
|
getRoot as getFlightRoot,
|
|
processBinaryChunk as processFlightBinaryChunk,
|
|
close as closeFlight,
|
|
} from 'react-client/src/ReactFlightClient';
|
|
|
|
import {
|
|
createRequest as createFizzRequest,
|
|
startWork as startFizzWork,
|
|
startFlowing as startFizzFlowing,
|
|
abort as abortFizz,
|
|
} from 'react-server/src/ReactFizzServer';
|
|
|
|
import {
|
|
createResumableState,
|
|
createRenderState,
|
|
createRootFormatContext,
|
|
} from './ReactFizzConfigHTML';
|
|
|
|
type ReactMarkupNodeList =
|
|
// This is the intersection of ReactNodeList and ReactClientValue minus
|
|
// Client/ServerReferences.
|
|
| React$Element<React$AbstractComponent<any, any>>
|
|
| LazyComponent<ReactMarkupNodeList, any>
|
|
| React$Element<string>
|
|
| string
|
|
| boolean
|
|
| number
|
|
| symbol
|
|
| null
|
|
| void
|
|
| bigint
|
|
| $AsyncIterable<ReactMarkupNodeList, ReactMarkupNodeList, void>
|
|
| $AsyncIterator<ReactMarkupNodeList, ReactMarkupNodeList, void>
|
|
| Iterable<ReactMarkupNodeList>
|
|
| Iterator<ReactMarkupNodeList>
|
|
| Array<ReactMarkupNodeList>
|
|
| Promise<ReactMarkupNodeList>; // Thenable<ReactMarkupNodeList>
|
|
|
|
type MarkupOptions = {
|
|
identifierPrefix?: string,
|
|
signal?: AbortSignal,
|
|
onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
|
|
};
|
|
|
|
function noServerCallOrFormAction() {
|
|
throw new Error(
|
|
'renderToMarkup should not have emitted Server References. This is a bug in React.',
|
|
);
|
|
}
|
|
|
|
export function renderToMarkup(
|
|
children: ReactMarkupNodeList,
|
|
options?: MarkupOptions,
|
|
): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const textEncoder = new TextEncoder();
|
|
const flightDestination = {
|
|
push(chunk: string | null): boolean {
|
|
if (chunk !== null) {
|
|
// TODO: Legacy should not use binary streams.
|
|
processFlightBinaryChunk(flightResponse, textEncoder.encode(chunk));
|
|
} else {
|
|
closeFlight(flightResponse);
|
|
}
|
|
return true;
|
|
},
|
|
destroy(error: mixed): void {
|
|
abortFizz(fizzRequest, error);
|
|
reject(error);
|
|
},
|
|
};
|
|
let buffer = '';
|
|
const fizzDestination = {
|
|
// $FlowFixMe[missing-local-annot]
|
|
push(chunk) {
|
|
if (chunk !== null) {
|
|
buffer += chunk;
|
|
} else {
|
|
// null indicates that we finished
|
|
resolve(buffer);
|
|
}
|
|
return true;
|
|
},
|
|
// $FlowFixMe[missing-local-annot]
|
|
destroy(error) {
|
|
abortFlight(flightRequest, error);
|
|
reject(error);
|
|
},
|
|
};
|
|
|
|
let stashErrorIdx = 1;
|
|
const stashedErrors: Map<string, mixed> = new Map();
|
|
|
|
function handleFlightError(error: mixed): string {
|
|
// For Flight errors we don't immediately reject, because they might not matter
|
|
// to the output of the HTML. We stash the error with a digest in case we need
|
|
// to get to the original error from the Fizz render.
|
|
const id = '' + stashErrorIdx++;
|
|
stashedErrors.set(id, error);
|
|
return id;
|
|
}
|
|
|
|
function handleError(error: mixed, errorInfo: ErrorInfo) {
|
|
if (typeof error === 'object' && error !== null) {
|
|
const id = error.digest;
|
|
// Note that the original error might be `undefined` so we need a has check.
|
|
if (typeof id === 'string' && stashedErrors.has(id)) {
|
|
// Get the original error thrown inside Flight.
|
|
error = stashedErrors.get(id);
|
|
}
|
|
}
|
|
|
|
// Any error rejects the promise, regardless of where it happened.
|
|
// Unlike other React SSR we don't want to put Suspense boundaries into
|
|
// client rendering mode because there's no client rendering here.
|
|
reject(error);
|
|
|
|
const onError = options && options.onError;
|
|
if (onError) {
|
|
if (__DEV__) {
|
|
const prevGetCurrentStackImpl =
|
|
ReactSharedInternalsServer.getCurrentStack;
|
|
// We're inside a "client" callback from Fizz but we only have access to the
|
|
// "server" runtime so to get access to a stack trace within this callback we
|
|
// need to override it to get it from the client runtime.
|
|
ReactSharedInternalsServer.getCurrentStack =
|
|
ReactSharedInternalsClient.getCurrentStack;
|
|
try {
|
|
onError(error, errorInfo);
|
|
} finally {
|
|
ReactSharedInternalsServer.getCurrentStack =
|
|
prevGetCurrentStackImpl;
|
|
}
|
|
} else {
|
|
onError(error, errorInfo);
|
|
}
|
|
}
|
|
}
|
|
const flightRequest = createFlightRequest(
|
|
// $FlowFixMe: This should be a subtype but not everything is typed covariant.
|
|
children,
|
|
null,
|
|
handleFlightError,
|
|
options ? options.identifierPrefix : undefined,
|
|
undefined,
|
|
'Markup',
|
|
undefined,
|
|
);
|
|
const flightResponse = createFlightResponse(
|
|
null,
|
|
null,
|
|
noServerCallOrFormAction,
|
|
noServerCallOrFormAction,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
const resumableState = createResumableState(
|
|
options ? options.identifierPrefix : undefined,
|
|
undefined,
|
|
);
|
|
const root = getFlightRoot<ReactNodeList>(flightResponse);
|
|
const fizzRequest = createFizzRequest(
|
|
// $FlowFixMe: Thenables as children are supported.
|
|
root,
|
|
resumableState,
|
|
createRenderState(
|
|
resumableState,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
),
|
|
createRootFormatContext(),
|
|
Infinity,
|
|
handleError,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
if (options && options.signal) {
|
|
const signal = options.signal;
|
|
if (signal.aborted) {
|
|
abortFlight(flightRequest, (signal: any).reason);
|
|
abortFizz(fizzRequest, (signal: any).reason);
|
|
} else {
|
|
const listener = () => {
|
|
abortFlight(flightRequest, (signal: any).reason);
|
|
abortFizz(fizzRequest, (signal: any).reason);
|
|
signal.removeEventListener('abort', listener);
|
|
};
|
|
signal.addEventListener('abort', listener);
|
|
}
|
|
}
|
|
startFlightWork(flightRequest);
|
|
startFlightFlowing(flightRequest, flightDestination);
|
|
startFizzWork(fizzRequest);
|
|
startFizzFlowing(fizzRequest, fizzDestination);
|
|
});
|
|
}
|
|
|
|
export {ReactVersion as version};
|