Skip to content

Truncate RPC error fields by byte length, not code unit count - #1254

Open
xianshijing-lk wants to merge 2 commits into
mainfrom
port/js-2126-truncate-surrogate-pair
Open

xianshijing-lk wants to merge 2 commits into
mainfrom
port/js-2126-truncate-surrogate-pair

Conversation

@xianshijing-lk

Copy link
Copy Markdown
Contributor

Port of livekit/client-sdk-js#2126.

The bug

RpcServerManager._truncateForWire caps the error message and data at kRpcMaxPayloadBytes (15360) like this:

final truncatedMessage = originalMessage.length > kRpcMaxPayloadBytes
    ? originalMessage.substring(0, kRpcMaxPayloadBytes)
    : originalMessage;

String.length counts UTF-16 code units, not bytes, which causes two distinct problems:

  1. The cap does not bound what it exists to bound. A message of non-ASCII text encodes to more bytes than it has code units, so it can still blow well past the 15 KB v1 packet budget. 8000 emoji measure 16000 "under the limit" but encode to 32000 bytes.
  2. substring can split a surrogate pair, sending its first half alone. That is not representable in UTF-8, so it reaches the peer as U+FFFD.

Repro

Added a case to test/core/rpc_test.dart that throws an oversize emoji message from an RPC handler and inspects what the caller receives. Against the unfixed code:

rpc v2 tests v2 oversize error message is truncated on a character boundary [E]
  Expected: false
    Actual: <true>
  truncation split a surrogate pair

The fix

Added lib/src/rpc/rpc_utils.dart with truncateBytes, mirroring the JS SDK's room/rpc/utils.ts: binary search for the longest prefix whose UTF-8 encoding fits, then step back off a trailing high surrogate. Both the message and data fields now use it.

This makes the cap byte-accurate as well as fixing the split, so the test asserts both properties.

Verification

flutter test — 467/467 pass (the new case fails before the change). flutter analyze lib/src/rpc/ clean.

Not needed elsewhere

Swift and Rust are safe by construction (grapheme Characters and char scalar values respectively). Android has the same class of bug, fixed in livekit/client-sdk-android#1044.

🤖 Generated with Claude Code

_truncateForWire compared String.length against kRpcMaxPayloadBytes and
sliced with substring. String.length counts UTF-16 code units, not
bytes, so the cap did not bound the wire packet it exists to bound: a
message of non-ASCII text could still encode to well over 15 KB. The
same slice could also land between the halves of a surrogate pair,
sending the pair's first half alone, which encodes as U+FFFD and reached
the peer corrupted.

Add truncateBytes, which binary searches for the longest prefix fitting
the byte budget and steps back off a trailing high surrogate, and use it
for both the message and the data field.

Port of livekit/client-sdk-js#2126.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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