Skip to content

[Flight] Prevent serialized size leaking across requests #33121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,44 @@ describe('ReactFlightDOMEdge', () => {
expect(html).toBe(html2);
});

it('regression: should not leak serialized size', async () => {
const MAX_ROW_SIZE = 3200;
// This test case is a bit convoluted and may no longer trigger the original bug.
// Originally, the size of `promisedText` was not cleaned up so the sync portion
// ended up being deferred immediately when we called `renderToReadableStream` again
// i.e. `result2.syncText` became a Lazy element on the second request.
const longText = 'd'.repeat(MAX_ROW_SIZE);
const promisedText = Promise.resolve(longText);
const model = {syncText: <p>{longText}</p>, promisedText};

const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(model),
);

const result = await ReactServerDOMClient.createFromReadableStream(stream, {
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
});

const stream2 = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(model),
);

const result2 = await ReactServerDOMClient.createFromReadableStream(
stream2,
{
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);

expect(result2.syncText).toEqual(result.syncText);
});

it('should be able to serialize any kind of typed array', async () => {
const buffer = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
Expand Down
23 changes: 11 additions & 12 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3926,18 +3926,9 @@ function emitChunk(
return;
}
// For anything else we need to try to serialize it using JSON.
// We stash the outer parent size so we can restore it when we exit.
const parentSerializedSize = serializedSize;
// We don't reset the serialized size counter from reentry because that indicates that we
// are outlining a model and we actually want to include that size into the parent since
// it will still block the parent row. It only restores to zero at the top of the stack.
try {
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
const json: string = stringify(value, task.toJSON);
emitModelChunk(request, task.id, json);
} finally {
serializedSize = parentSerializedSize;
}
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
const json: string = stringify(value, task.toJSON);
emitModelChunk(request, task.id, json);
}

function erroredTask(request: Request, task: Task, error: mixed): void {
Expand Down Expand Up @@ -3975,6 +3966,11 @@ function retryTask(request: Request, task: Task): void {
const prevDebugID = debugID;
task.status = RENDERING;

// We stash the outer parent size so we can restore it when we exit.
const parentSerializedSize = serializedSize;
// We don't reset the serialized size counter from reentry because that indicates that we
// are outlining a model and we actually want to include that size into the parent since
// it will still block the parent row. It only restores to zero at the top of the stack.
try {
// Track the root so we know that we have to emit this object even though it
// already has an ID. This is needed because we might see this object twice
Expand Down Expand Up @@ -4086,6 +4082,7 @@ function retryTask(request: Request, task: Task): void {
if (__DEV__) {
debugID = prevDebugID;
}
serializedSize = parentSerializedSize;
}
}

Expand All @@ -4098,9 +4095,11 @@ function tryStreamTask(request: Request, task: Task): void {
// so that we instead outline the row to get a new debugID if needed.
debugID = null;
}
const parentSerializedSize = serializedSize;
try {
emitChunk(request, task, task.model);
} finally {
serializedSize = parentSerializedSize;
if (__DEV__) {
debugID = prevDebugID;
}
Expand Down