Describe the bug
What we saw
A client in production received an unparseable JSON payload over an uncompressed ByteStream. We have not been able to conclusively confirm the cause, and we're filing this as a suspected one: a missing or reordered chunk in the ByteStream.
The bug
ByteStreamReader and TextStreamReader never look at DataStream_Chunk.chunkIndex. Chunks are concatenated in arrival order, so a missing chunk produces a payload with a hole in the middle and a reordered pair produces bytes in the wrong order. In both cases readAll() resolves successfully. Application code receives silently corrupted data from a call that reported success, with no error, no event, and nothing on the reader it could check to find out.
The compressed path already enforces this invariant. ensureOrderedChunks errors with DataStreamErrorReason.Incomplete on a gap and drops duplicates. Uncompressed streams carry the same chunks over the same channel and get no equivalent check: handleStreamChunk enqueues unconditionally, so nothing on the uncompressed path ever reads the index.
Reachability
We went looking for how a chunk could go missing in the first place, and could not close the loop from the client side. Data stream packets go over DataChannelKind.RELIABLE, and both reconnect paths appear covered: a full reconnect errors in-flight streams, and a resume relies on the SFU replaying missed reliable packets from the datachannelReceiveStates cursors the client sends in sync state. The resume replay is the one link we can't check: whether it is ever short is a server-side question we have no visibility into.
Nothing would catch it if that happened. handleDataMessage drops packets with sequence <= lastSeq to absorb replayed duplicates, but never flags sequence > lastSeq + 1, so a short replay is ignored at the engine layer, which is the layer that knows a replay occurred.
Reproduction
Deterministic at the manager level; src/room/data-stream/incoming/IncomingDataStreamManager.test.ts already has the harness. Register a stream handler, feed a streamHeader, feed chunks where chunk i carries byte/character i, feed a streamTrailer, then await reader.readAll().
|
totalLength |
chunks delivered |
expected |
actual |
| A |
none |
0, 1, 3, 4 |
reject |
resolves [0,1,3,4] |
| B |
none |
0, 1, 3, 2, 4 |
reject |
resolves [0,1,3,2,4] |
| C |
5 |
0, 1, 3, 4 |
reject |
rejects (Incomplete) ✓ |
| D |
5 |
0, 1, 3, 2, 4 |
reject |
resolves [0,1,3,2,4] |
Case C is the only one caught today, and it is caught by byte count rather than by index. Text streams behave identically to A and B ("0134" and "01324").
Logs
System Info
Browsers:
Electron 43.0.0 (Chromium), macOS
npmPackages:
livekit-client: 2.18.6 (version of the production sighting)
Server: LiveKit Cloud, US East 1, UDP
Severity
serious, but I can work around it
Additional Information
Use ensureOrderedChunks on the uncompressed byte and text paths.
Describe the bug
What we saw
A client in production received an unparseable JSON payload over an uncompressed
ByteStream. We have not been able to conclusively confirm the cause, and we're filing this as a suspected one: a missing or reordered chunk in theByteStream.The bug
ByteStreamReaderandTextStreamReadernever look atDataStream_Chunk.chunkIndex. Chunks are concatenated in arrival order, so a missing chunk produces a payload with a hole in the middle and a reordered pair produces bytes in the wrong order. In both casesreadAll()resolves successfully. Application code receives silently corrupted data from a call that reported success, with no error, no event, and nothing on the reader it could check to find out.The compressed path already enforces this invariant.
ensureOrderedChunkserrors withDataStreamErrorReason.Incompleteon a gap and drops duplicates. Uncompressed streams carry the same chunks over the same channel and get no equivalent check:handleStreamChunkenqueues unconditionally, so nothing on the uncompressed path ever reads the index.Reachability
We went looking for how a chunk could go missing in the first place, and could not close the loop from the client side. Data stream packets go over
DataChannelKind.RELIABLE, and both reconnect paths appear covered: a full reconnect errors in-flight streams, and a resume relies on the SFU replaying missed reliable packets from thedatachannelReceiveStatescursors the client sends in sync state. The resume replay is the one link we can't check: whether it is ever short is a server-side question we have no visibility into.Nothing would catch it if that happened.
handleDataMessagedrops packets withsequence <= lastSeqto absorb replayed duplicates, but never flagssequence > lastSeq + 1, so a short replay is ignored at the engine layer, which is the layer that knows a replay occurred.Reproduction
Deterministic at the manager level;
src/room/data-stream/incoming/IncomingDataStreamManager.test.tsalready has the harness. Register a stream handler, feed astreamHeader, feed chunks where chunk i carries byte/characteri, feed astreamTrailer, thenawait reader.readAll().totalLength[0,1,3,4][0,1,3,2,4]Incomplete) ✓[0,1,3,2,4]Case C is the only one caught today, and it is caught by byte count rather than by index. Text streams behave identically to A and B (
"0134"and"01324").Logs
System Info
Severity
serious, but I can work around it
Additional Information
Use
ensureOrderedChunkson the uncompressed byte and text paths.