From bc896c65dc0145597a34913200cdf0165ce4268a Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 19 Sep 2025 22:45:42 +0200 Subject: [PATCH 1/2] [Flight] Handle Lazy in `renderDebugModel` If we don't handle Lazy types specifically in `renderDebugModel`, all of their properties will be emitted using `renderDebugModel` as well. This also includes its `_debugInfo` property, if the Lazy comes from the Flight client. That array might contain objects that are deduped, and resolving those references in the client can cause runtime errors, e.g.: ``` TypeError: Cannot read properties of undefined (reading '$$typeof') ``` This happened specifically when an "RSC stream" debug info entry, coming from the Flight client through IO tracking, was emitted and its `debugTask` property was deduped, which couldn't be resolved in the client. To avoid actually initializing a lazy causing a side-effect, we make some assumptions about the structure of its payload, and only emit resolved or rejected values, otherwise we emit a halted chunk. --- .../react-server/src/ReactFlightServer.js | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 31bea759a0a..12a33290704 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -4702,6 +4702,59 @@ function renderDebugModel( element._store.validated, ]; } + case REACT_LAZY_TYPE: { + // To avoid actually initializing a lazy causing a side-effect, we make + // some assumptions about the structure of the payload even though + // that's not really part of the contract. In practice, this is really + // just coming from React.lazy helper or Flight. + const lazy: LazyComponent = (value: any); + const payload = lazy._payload; + + if (payload !== null && typeof payload === 'object') { + // React.lazy constructor + switch (payload._status) { + case -1 /* Uninitialized */: + case 0 /* Pending */: + break; + case 1 /* Resolved */: + case 2 /* Rejected */: { + const id = outlineDebugModel(request, counter, payload._result); + return serializeLazyID(id); + } + } + + // React Flight + switch (payload.status) { + case 'pending': + case 'blocked': + case 'resolved_model': + // The value is an uninitialized model from the Flight client. + // It's not very useful to emit that. + break; + case 'resolved_module': + // The value is client reference metadata from the Flight client. + // It's likely for SSR, so we chose not to emit it. + break; + case 'fulfilled': { + const id = outlineDebugModel(request, counter, payload.value); + return serializeLazyID(id); + } + case 'rejected': { + const id = outlineDebugModel(request, counter, payload.reason); + return serializeLazyID(id); + } + } + } + + // We couldn't emit a resolved or rejected value synchronously. For now, + // we emit this as a halted chunk. TODO: We could maybe also handle + // pending lazy debug models like we do in serializeDebugThenable, + // if/when we determine that it's worth the added complexity. + request.pendingDebugChunks++; + const id = request.nextChunkId++; + emitDebugHaltChunk(request, id); + return serializeLazyID(id); + } } // $FlowFixMe[method-unbinding] From 8c5e017d091fce1dde11c8ad19c04ab8fe6bf383 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 19 Sep 2025 23:22:59 +0200 Subject: [PATCH 2/2] Emit error chunk if Lazy is rejected --- .../react-server/src/ReactFlightServer.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 12a33290704..66203af1fef 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -4716,11 +4716,18 @@ function renderDebugModel( case -1 /* Uninitialized */: case 0 /* Pending */: break; - case 1 /* Resolved */: - case 2 /* Rejected */: { + case 1 /* Resolved */: { const id = outlineDebugModel(request, counter, payload._result); return serializeLazyID(id); } + case 2 /* Rejected */: { + // We don't log these errors since they didn't actually throw into + // Flight. + const digest = ''; + const id = request.nextChunkId++; + emitErrorChunk(request, id, digest, payload._result, true, null); + return serializeLazyID(id); + } } // React Flight @@ -4733,14 +4740,18 @@ function renderDebugModel( break; case 'resolved_module': // The value is client reference metadata from the Flight client. - // It's likely for SSR, so we chose not to emit it. + // It's likely for SSR, so we choose not to emit it. break; case 'fulfilled': { const id = outlineDebugModel(request, counter, payload.value); return serializeLazyID(id); } case 'rejected': { - const id = outlineDebugModel(request, counter, payload.reason); + // We don't log these errors since they didn't actually throw into + // Flight. + const digest = ''; + const id = request.nextChunkId++; + emitErrorChunk(request, id, digest, payload.reason, true, null); return serializeLazyID(id); } }