Skip to content

Commit bc896c6

Browse files
committed
[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.
1 parent 6eda534 commit bc896c6

File tree

1 file changed

+53
-0
lines changed

1 file changed

+53
-0
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4702,6 +4702,59 @@ function renderDebugModel(
47024702
element._store.validated,
47034703
];
47044704
}
4705+
case REACT_LAZY_TYPE: {
4706+
// To avoid actually initializing a lazy causing a side-effect, we make
4707+
// some assumptions about the structure of the payload even though
4708+
// that's not really part of the contract. In practice, this is really
4709+
// just coming from React.lazy helper or Flight.
4710+
const lazy: LazyComponent<any, any> = (value: any);
4711+
const payload = lazy._payload;
4712+
4713+
if (payload !== null && typeof payload === 'object') {
4714+
// React.lazy constructor
4715+
switch (payload._status) {
4716+
case -1 /* Uninitialized */:
4717+
case 0 /* Pending */:
4718+
break;
4719+
case 1 /* Resolved */:
4720+
case 2 /* Rejected */: {
4721+
const id = outlineDebugModel(request, counter, payload._result);
4722+
return serializeLazyID(id);
4723+
}
4724+
}
4725+
4726+
// React Flight
4727+
switch (payload.status) {
4728+
case 'pending':
4729+
case 'blocked':
4730+
case 'resolved_model':
4731+
// The value is an uninitialized model from the Flight client.
4732+
// It's not very useful to emit that.
4733+
break;
4734+
case 'resolved_module':
4735+
// The value is client reference metadata from the Flight client.
4736+
// It's likely for SSR, so we chose not to emit it.
4737+
break;
4738+
case 'fulfilled': {
4739+
const id = outlineDebugModel(request, counter, payload.value);
4740+
return serializeLazyID(id);
4741+
}
4742+
case 'rejected': {
4743+
const id = outlineDebugModel(request, counter, payload.reason);
4744+
return serializeLazyID(id);
4745+
}
4746+
}
4747+
}
4748+
4749+
// We couldn't emit a resolved or rejected value synchronously. For now,
4750+
// we emit this as a halted chunk. TODO: We could maybe also handle
4751+
// pending lazy debug models like we do in serializeDebugThenable,
4752+
// if/when we determine that it's worth the added complexity.
4753+
request.pendingDebugChunks++;
4754+
const id = request.nextChunkId++;
4755+
emitDebugHaltChunk(request, id);
4756+
return serializeLazyID(id);
4757+
}
47054758
}
47064759

47074760
// $FlowFixMe[method-unbinding]

0 commit comments

Comments
 (0)