Thanks to visit codestin.com
Credit goes to github.com

Skip to content

sdk: H3Transport reassemble frames across reads#934

Merged
endel merged 1 commit into
colyseus:masterfrom
anaibol:fix/h3transport-frame-reassembly
Apr 24, 2026
Merged

sdk: H3Transport reassemble frames across reads#934
endel merged 1 commit into
colyseus:masterfrom
anaibol:fix/h3transport-frame-reassembly

Conversation

@anaibol

@anaibol anaibol commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Problem

H3TransportTransport.readIncomingData() (and its unreliable counterpart) in packages/sdk/src/transport/H3Transport.ts assumes every reader.read() chunk contains one or more whole length-prefixed frames:

const messages = result.value;
const it: Iterator = { offset: 0 };
do {
    // QUESTION: should we buffer the message in case it's not fully read?
    const length = decode.number(messages as any, it);
    this.events.onmessage({ data: messages.subarray(it.offset, it.offset + length) });
    it.offset += length;
} while (it.offset < messages.length);

With WebTransport over HTTP/3, a single stream read is not guaranteed to land on a frame boundary. A chunk can:

  1. End mid-payloadonmessage is called with a truncated buffer and the schema decoder then fails on the next patch with partial-decode errors (stale refId, missing fields, "no welcome data" during handshake).
  2. End inside the varint length prefixdecode.number throws and the whole read loop aborts.

In practice this manifests as sporadic handshake failures and ROOM_STATE_PATCH decode errors on rooms with larger initial state, especially under dev / local latency where chunks are naturally fragmented.

Fix

Introduce a FrameReassembler that buffers a pending byte tail across reads and only emits fully-received frames. Both the reliable and unreliable readers feed a reassembler instance and dispatch complete frames to onmessage.

This answers the existing // QUESTION: should we buffer the message in case it's not fully read? TODO — yes, we should.

No change to the wire format, message ordering, or public API of H3TransportTransport. FrameReassembler is exported so it can be unit-tested directly; it's a narrow helper class.

Test plan

Unit tests in packages/sdk/test/h3transport.test.ts cover:

  • single whole frame in one chunk
  • multiple whole frames packed in one chunk
  • payload split across two reads
  • multi-byte varint length prefix split across two reads
  • mixed whole + partial frames across reads
  • empty / undefined chunks (no-op)
 RUN  v2.1.9 /private/tmp/colyseus/packages/sdk
 ✓ test/h3transport.test.ts (6 tests) 2ms
 Test Files  1 passed (1)
      Tests  6 passed (6)

Notes

Originally scoped for colyseus/colyseus.js, but that repo is archived; active development lives in this monorepo at packages/sdk. The bug is present in both.

A single WebTransport reader.read() chunk is not guaranteed to contain
whole length-prefixed frames — a frame payload, or even its varint length
prefix, may be split across multiple reads. The previous loop assumed every
chunk started and ended on a frame boundary, which caused decoded frames to
be truncated (or length-decode to throw) when the underlying stream
fragmented, surfacing downstream as partial-decode errors on schema patches
(missing welcome data / refId errors).

Introduce a FrameReassembler that buffers a pending byte tail across reads
and only emits complete frames. Both the reliable and unreliable readers
now feed a reassembler and dispatch whole frames to onmessage().

Answers the existing "should we buffer the message in case it's not fully
read?" TODO in the code.
@endel endel closed this in 3a62b72 Apr 24, 2026
@endel endel merged commit 1886a36 into colyseus:master Apr 24, 2026
@endel

endel commented Apr 24, 2026

Copy link
Copy Markdown
Member

Thank you for reporting @anaibol. We had the same issue in the server-side - I've applied the same FrameReassembler technique at the server. Just published new versions of @colyseus/sdk and @colyseus/h3-transport 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants