[Flight] Prevent serialized size leaking across requests (#33121)
Sebastian "Sebbie" Silbermann committed
May 5, 2025 at 18:30 UTC
b9cfa0d3083f80bdd11ba76a55aa08fa659b7359
2 files changed
+49
-12
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+38
@@ -692,6 +692,44 @@ describe('ReactFlightDOMEdge', () => {
692
expect(html).toBe(html2);
693
});
694
695
+ it('regression: should not leak serialized size', async () => {
696
+ const MAX_ROW_SIZE = 3200;
697
+ // This test case is a bit convoluted and may no longer trigger the original bug.
698
+ // Originally, the size of `promisedText` was not cleaned up so the sync portion
699
+ // ended up being deferred immediately when we called `renderToReadableStream` again
700
+ // i.e. `result2.syncText` became a Lazy element on the second request.
701
+ const longText = 'd'.repeat(MAX_ROW_SIZE);
702
+ const promisedText = Promise.resolve(longText);
703
+ const model = {syncText: <p>{longText}</p>, promisedText};
704
+
705
+ const stream = await serverAct(() =>
706
+ ReactServerDOMServer.renderToReadableStream(model),
707
+ );
708
+
709
+ const result = await ReactServerDOMClient.createFromReadableStream(stream, {
710
+ serverConsumerManifest: {
711
+ moduleMap: null,
712
+ moduleLoading: null,
713
+ },
714
+ });
715
+
716
+ const stream2 = await serverAct(() =>
717
+ ReactServerDOMServer.renderToReadableStream(model),
718
+ );
719
+
720
+ const result2 = await ReactServerDOMClient.createFromReadableStream(
721
+ stream2,
722
+ {
723
+ serverConsumerManifest: {
724
+ moduleMap: null,
725
+ moduleLoading: null,
726
+ },
727
+ },
728
+ );
729
+
730
+ expect(result2.syncText).toEqual(result.syncText);
731
+ });
732
+
733
it('should be able to serialize any kind of typed array', async () => {
734
const buffer = new Uint8Array([
735
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
packages/react-server/src/ReactFlightServer.js
+11
-12
@@ -3927,18 +3927,9 @@ function emitChunk(
3927
return;
3928
}
3929
// For anything else we need to try to serialize it using JSON.
3930
- // We stash the outer parent size so we can restore it when we exit.
3931
- const parentSerializedSize = serializedSize;
3932
- // We don't reset the serialized size counter from reentry because that indicates that we
3933
- // are outlining a model and we actually want to include that size into the parent since
3934
- // it will still block the parent row. It only restores to zero at the top of the stack.
3935
- try {
3936
- // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
3937
- const json: string = stringify(value, task.toJSON);
3938
- emitModelChunk(request, task.id, json);
3939
- } finally {
3940
- serializedSize = parentSerializedSize;
3941
- }
3930
+ // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
3931
+ const json: string = stringify(value, task.toJSON);
3932
+ emitModelChunk(request, task.id, json);
3933
}
3934
3935
function erroredTask(request: Request, task: Task, error: mixed): void {
@@ -3976,6 +3967,11 @@ function retryTask(request: Request, task: Task): void {
3967
const prevDebugID = debugID;
3968
task.status = RENDERING;
3969
3970
+ // We stash the outer parent size so we can restore it when we exit.
3971
+ const parentSerializedSize = serializedSize;
3972
+ // We don't reset the serialized size counter from reentry because that indicates that we
3973
+ // are outlining a model and we actually want to include that size into the parent since
3974
+ // it will still block the parent row. It only restores to zero at the top of the stack.
3975
try {
3976
// Track the root so we know that we have to emit this object even though it
3977
// already has an ID. This is needed because we might see this object twice
@@ -4087,6 +4083,7 @@ function retryTask(request: Request, task: Task): void {
4083
if (__DEV__) {
4084
debugID = prevDebugID;
4085
}
4086
+ serializedSize = parentSerializedSize;
4087
}
4088
}
4089
@@ -4099,9 +4096,11 @@ function tryStreamTask(request: Request, task: Task): void {
4096
// so that we instead outline the row to get a new debugID if needed.
4097
debugID = null;
4098
}
4099
+ const parentSerializedSize = serializedSize;
4100
try {
4101
emitChunk(request, task, task.model);
4102
} finally {
4103
+ serializedSize = parentSerializedSize;
4104
if (__DEV__) {
4105
debugID = prevDebugID;
4106
}