Skip to content

Commit d415fd3

Browse files
authored
[Flight] Handle Lazy in renderDebugModel (#34536)
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 5e3cd53 commit d415fd3

File tree

1 file changed

+64
-0
lines changed

1 file changed

+64
-0
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4702,6 +4702,70 @@ 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+
const id = outlineDebugModel(request, counter, payload._result);
4721+
return serializeLazyID(id);
4722+
}
4723+
case 2 /* Rejected */: {
4724+
// We don't log these errors since they didn't actually throw into
4725+
// Flight.
4726+
const digest = '';
4727+
const id = request.nextChunkId++;
4728+
emitErrorChunk(request, id, digest, payload._result, true, null);
4729+
return serializeLazyID(id);
4730+
}
4731+
}
4732+
4733+
// React Flight
4734+
switch (payload.status) {
4735+
case 'pending':
4736+
case 'blocked':
4737+
case 'resolved_model':
4738+
// The value is an uninitialized model from the Flight client.
4739+
// It's not very useful to emit that.
4740+
break;
4741+
case 'resolved_module':
4742+
// The value is client reference metadata from the Flight client.
4743+
// It's likely for SSR, so we choose not to emit it.
4744+
break;
4745+
case 'fulfilled': {
4746+
const id = outlineDebugModel(request, counter, payload.value);
4747+
return serializeLazyID(id);
4748+
}
4749+
case 'rejected': {
4750+
// We don't log these errors since they didn't actually throw into
4751+
// Flight.
4752+
const digest = '';
4753+
const id = request.nextChunkId++;
4754+
emitErrorChunk(request, id, digest, payload.reason, true, null);
4755+
return serializeLazyID(id);
4756+
}
4757+
}
4758+
}
4759+
4760+
// We couldn't emit a resolved or rejected value synchronously. For now,
4761+
// we emit this as a halted chunk. TODO: We could maybe also handle
4762+
// pending lazy debug models like we do in serializeDebugThenable,
4763+
// if/when we determine that it's worth the added complexity.
4764+
request.pendingDebugChunks++;
4765+
const id = request.nextChunkId++;
4766+
emitDebugHaltChunk(request, id);
4767+
return serializeLazyID(id);
4768+
}
47054769
}
47064770

47074771
// $FlowFixMe[method-unbinding]

0 commit comments

Comments
 (0)