From 0d9a6df37336a59799e150fc3f7a15a58804045f Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 13 Feb 2026 16:41:56 +0100 Subject: [PATCH 1/8] [Flight] Walk parsed JSON instead of using reviver for parsing RSC payload --- .../react-client/src/ReactFlightClient.js | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 20aa8ce8f9a3..3e325bb6530d 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -355,7 +355,7 @@ type Response = { _encodeFormAction: void | EncodeFormActionCallback, _nonce: ?string, _chunks: Map>, - _fromJSON: (key: string, value: JSONValue) => any, + _walkJSON: (value: JSONValue, parentObject: Object, key: string) => any, _stringDecoder: StringDecoder, _closed: boolean, _closedReason: mixed, @@ -2683,7 +2683,7 @@ function ResponseInstance( this._nonce = nonce; this._chunks = chunks; this._stringDecoder = createStringDecoder(); - this._fromJSON = (null: any); + this._walkJSON = (null: any); this._closed = false; this._closedReason = null; this._allowPartialStream = allowPartialStream; @@ -2769,7 +2769,7 @@ function ResponseInstance( } // Don't inline this call because it causes closure to outline the call above. - this._fromJSON = createFromJSONCallback(this); + this._walkJSON = createWalkParsedJSON(this); } export function createResponse( @@ -5237,24 +5237,50 @@ export function processStringChunk( } function parseModel(response: Response, json: UninitializedModel): T { - return JSON.parse(json, response._fromJSON); + return response._walkJSON(JSON.parse(json), null, ''); } -function createFromJSONCallback(response: Response) { - // $FlowFixMe[missing-this-annot] - return function (key: string, value: JSONValue) { - if (key === __PROTO__) { - return undefined; - } +function createWalkParsedJSON(response: Response) { + function walk( + value: JSONValue, + parentObject: Object | null, + key: string, + ): any { if (typeof value === 'string') { - // We can't use .bind here because we need the "this" value. - return parseModelString(response, this, key, value); + if (value[0] === '$') { + return parseModelString(response, parentObject, key, value); + } + return value; } - if (typeof value === 'object' && value !== null) { - return parseModelTuple(response, value); + if (typeof value !== 'object' || value === null) { + return value; + } + if (Array.isArray(value)) { + if (value[0] === REACT_ELEMENT_TYPE) { + // React element tuple + return parseModelTuple(response, value); + } + for (let i = 0; i < value.length; i++) { + (value: any)[i] = walk(value[i], value, '' + i); + } + return value; + } + // Plain object + for (const k in value) { + if (k === __PROTO__) { + delete (value: any)[k]; + } else { + const walked = walk((value: any)[k], value, k); + if (walked !== undefined) { + (value: any)[k] = walked; + } else { + delete (value: any)[k]; + } + } } return value; - }; + } + return walk; } export function close(weakResponse: WeakResponse): void { From 7bac910b75a6a9ed721bbe1c5b5f08779081630f Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 13 Feb 2026 18:19:38 +0100 Subject: [PATCH 2/8] Fix tests --- packages/react-client/src/ReactFlightClient.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 3e325bb6530d..dd78261e7dce 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -5237,7 +5237,11 @@ export function processStringChunk( } function parseModel(response: Response, json: UninitializedModel): T { - return response._walkJSON(JSON.parse(json), null, ''); + const parsed = JSON.parse(json); + // Pass a wrapper object as parentObject to match the original JSON.parse + // reviver behavior, where the root value's reviver receives {"": rootValue} + // as `this`. This ensures parentObject is never null when accessed downstream. + return response._walkJSON(parsed, {'': parsed}, ''); } function createWalkParsedJSON(response: Response) { @@ -5256,13 +5260,13 @@ function createWalkParsedJSON(response: Response) { return value; } if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + (value: any)[i] = walk(value[i], value, '' + i); + } if (value[0] === REACT_ELEMENT_TYPE) { // React element tuple return parseModelTuple(response, value); } - for (let i = 0; i < value.length; i++) { - (value: any)[i] = walk(value[i], value, '' + i); - } return value; } // Plain object From 6b213da3a54e09643b02406fd0d978268ac07383 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 13 Feb 2026 20:26:25 +0100 Subject: [PATCH 3/8] Update ReactNoopFlightClient.js --- packages/react-noop-renderer/src/ReactNoopFlightClient.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index a5c43bd65259..0e9c7e8ad4d6 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -44,7 +44,8 @@ const {createResponse, createStreamState, processBinaryChunk, getRoot, close} = return readModule(idx); }, parseModel(response: Response, json) { - return JSON.parse(json, response._fromJSON); + const parsed = JSON.parse(json); + return response._walkJSON(parsed, {'': parsed}, ''); }, bindToConsole(methodName, args, badgeName) { return Function.prototype.bind.apply( From 7d7682c5e3e159297642056bb5867f5cfff6c512 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 13 Feb 2026 20:42:37 +0100 Subject: [PATCH 4/8] Update ReactFlightClient.js --- packages/react-client/src/ReactFlightClient.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index dd78261e7dce..cfd6e01cc19c 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -2286,6 +2286,11 @@ function defineLazyGetter( // TODO: We should ideally throw here to indicate a difference. return OMITTED_PROP_ERROR; }, + // no-op: the walk function may try to reassign this property after + // parseModelString returns. With the JSON.parse reviver, the engine's + // internal CreateDataProperty silently failed. We use a no-op setter + // to match that behavior in strict mode. + set: function () {}, enumerable: true, configurable: false, }); @@ -2606,6 +2611,11 @@ function parseModelString( // TODO: We should ideally throw here to indicate a difference. return OMITTED_PROP_ERROR; }, + // no-op: the walk function may try to reassign this property + // after parseModelString returns. With the JSON.parse reviver, + // the engine's internal CreateDataProperty silently failed. + // We use a no-op setter to match that behavior in strict mode. + set: function () {}, enumerable: true, configurable: false, }); From 1831decb29120d0407ba2c499f8c0fe6b3e1d0ae Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 16 Feb 2026 11:33:18 +0100 Subject: [PATCH 5/8] Apply feedback --- .../react-client/src/ReactFlightClient.js | 73 +++++++++---------- .../src/ReactNoopFlightClient.js | 4 - 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index cfd6e01cc19c..5f85181985e9 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -355,7 +355,6 @@ type Response = { _encodeFormAction: void | EncodeFormActionCallback, _nonce: ?string, _chunks: Map>, - _walkJSON: (value: JSONValue, parentObject: Object, key: string) => any, _stringDecoder: StringDecoder, _closed: boolean, _closedReason: mixed, @@ -2693,7 +2692,6 @@ function ResponseInstance( this._nonce = nonce; this._chunks = chunks; this._stringDecoder = createStringDecoder(); - this._walkJSON = (null: any); this._closed = false; this._closedReason = null; this._allowPartialStream = allowPartialStream; @@ -2777,9 +2775,6 @@ function ResponseInstance( markAllTracksInOrder(); } } - - // Don't inline this call because it causes closure to outline the call above. - this._walkJSON = createWalkParsedJSON(this); } export function createResponse( @@ -5251,50 +5246,48 @@ function parseModel(response: Response, json: UninitializedModel): T { // Pass a wrapper object as parentObject to match the original JSON.parse // reviver behavior, where the root value's reviver receives {"": rootValue} // as `this`. This ensures parentObject is never null when accessed downstream. - return response._walkJSON(parsed, {'': parsed}, ''); + return walkParsedJSON(response, parsed, {'': parsed}, ''); } -function createWalkParsedJSON(response: Response) { - function walk( - value: JSONValue, - parentObject: Object | null, - key: string, - ): any { - if (typeof value === 'string') { - if (value[0] === '$') { - return parseModelString(response, parentObject, key, value); - } - return value; +function walkParsedJSON( + response: Response, + value: JSONValue, + parentObject: Object | null, + key: string, +): any { + if (typeof value === 'string') { + if (value[0] === '$') { + return parseModelString(response, parentObject, key, value); } - if (typeof value !== 'object' || value === null) { - return value; + return value; + } + if (typeof value !== 'object' || value === null) { + return value; + } + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + (value: any)[i] = walkParsedJSON(response, value[i], value, '' + i); } - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - (value: any)[i] = walk(value[i], value, '' + i); - } - if (value[0] === REACT_ELEMENT_TYPE) { - // React element tuple - return parseModelTuple(response, value); - } - return value; + if (value[0] === REACT_ELEMENT_TYPE) { + // React element tuple + return parseModelTuple(response, value); } - // Plain object - for (const k in value) { - if (k === __PROTO__) { - delete (value: any)[k]; + return value; + } + // Plain object + for (const k in value) { + if (k === __PROTO__) { + delete (value: any)[k]; + } else { + const walked = walkParsedJSON(response, (value: any)[k], value, k); + if (walked !== undefined) { + (value: any)[k] = walked; } else { - const walked = walk((value: any)[k], value, k); - if (walked !== undefined) { - (value: any)[k] = walked; - } else { - delete (value: any)[k]; - } + delete (value: any)[k]; } } - return value; } - return walk; + return value; } export function close(weakResponse: WeakResponse): void { diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index 0e9c7e8ad4d6..4699b149e85f 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -43,10 +43,6 @@ const {createResponse, createStreamState, processBinaryChunk, getRoot, close} = requireModule(idx: string) { return readModule(idx); }, - parseModel(response: Response, json) { - const parsed = JSON.parse(json); - return response._walkJSON(parsed, {'': parsed}, ''); - }, bindToConsole(methodName, args, badgeName) { return Function.prototype.bind.apply( // eslint-disable-next-line react-internal/no-production-logging From c19747665d0684d389136e58a118d488c4d92300 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 17 Feb 2026 10:00:02 +0100 Subject: [PATCH 6/8] Rename to reviveModel --- packages/react-client/src/ReactFlightClient.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 5f85181985e9..96bc471c9230 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -5242,14 +5242,14 @@ export function processStringChunk( } function parseModel(response: Response, json: UninitializedModel): T { - const parsed = JSON.parse(json); + const rawModel = JSON.parse(json); // Pass a wrapper object as parentObject to match the original JSON.parse // reviver behavior, where the root value's reviver receives {"": rootValue} // as `this`. This ensures parentObject is never null when accessed downstream. - return walkParsedJSON(response, parsed, {'': parsed}, ''); + return reviveModel(response, rawModel, {'': rawModel}, ''); } -function walkParsedJSON( +function reviveModel( response: Response, value: JSONValue, parentObject: Object | null, @@ -5266,7 +5266,7 @@ function walkParsedJSON( } if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { - (value: any)[i] = walkParsedJSON(response, value[i], value, '' + i); + (value: any)[i] = reviveModel(response, value[i], value, '' + i); } if (value[0] === REACT_ELEMENT_TYPE) { // React element tuple @@ -5279,7 +5279,7 @@ function walkParsedJSON( if (k === __PROTO__) { delete (value: any)[k]; } else { - const walked = walkParsedJSON(response, (value: any)[k], value, k); + const walked = reviveModel(response, (value: any)[k], value, k); if (walked !== undefined) { (value: any)[k] = walked; } else { From bc7df67e54ed3b383c5105010bbaf426bb259f9b Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 17 Feb 2026 13:44:09 +0100 Subject: [PATCH 7/8] Update packages/react-client/src/ReactFlightClient.js Co-authored-by: Hendrik Liebau --- packages/react-client/src/ReactFlightClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 96bc471c9230..c34fa0700ce9 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -5252,7 +5252,7 @@ function parseModel(response: Response, json: UninitializedModel): T { function reviveModel( response: Response, value: JSONValue, - parentObject: Object | null, + parentObject: Object, key: string, ): any { if (typeof value === 'string') { From 0ff0870294995ba6500078666461417d6a66bd48 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 17 Feb 2026 13:44:18 +0100 Subject: [PATCH 8/8] Update packages/react-client/src/ReactFlightClient.js Co-authored-by: Hendrik Liebau --- packages/react-client/src/ReactFlightClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index c34fa0700ce9..5512a75a5043 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -5264,7 +5264,7 @@ function reviveModel( if (typeof value !== 'object' || value === null) { return value; } - if (Array.isArray(value)) { + if (isArray(value)) { for (let i = 0; i < value.length; i++) { (value: any)[i] = reviveModel(response, value[i], value, '' + i); }