@samitouri / QOS-React / commits / b9cfa0d308

[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 }