Fix join_request encoding and add gzip compression to the v1 signal URL - #1110
Conversation
The v1 signal path carries the JoinRequest protobuf as a query parameter, and Utils.buildWrappedJoinRequest diverged from client-sdk-js and rust-sdks in two ways. It emitted standard base64, whose `+` and `/` URLComponents leaves unescaped in a query value. A receiver that parses the query as form-urlencoded decodes `+` as a space, corrupting the payload. Both other SDKs deliberately use the URL-safe alphabet. It also hardcoded compression to .none. The request travels in the WebSocket upgrade URL, so an oversized request splits across TCP segments and a single loss costs a retransmission timeout before the handshake starts. Rust and JS both gzip, keeping the compressed form only when it is actually smaller. Apple's Compression framework has no gzip container -- COMPRESSION_ZLIB produces raw DEFLATE -- so Gzip wraps it in RFC 1952 framing with a CRC-32 rather than linking zlib for its container alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Validated against staging CloudRe-ran after opening, against a real LiveKit Cloud staging deployment ( This exercises the base64url fix on a real Cloud SFU, which is the confirmation the description flagged as outstanding — the encoding concern was verified in Foundation but not end to end at the time of opening. Note the gzip half still makes no difference on the wire by itself: a minimal join request is smaller than gzip's framing, so it correctly stays uncompressed (covered by Run with: TEST_RUNNER_LIVEKIT_TESTING_URL=wss://… TEST_RUNNER_LIVEKIT_TESTING_API_KEY=… \
TEST_RUNNER_LIVEKIT_TESTING_API_SECRET=… \
xcodebuild test -scheme LiveKit -only-testing LiveKitCoreTests/PeerConnectionSignalingTests -destination 'platform=macOS'(the |
Fixes CLT-2201.
The v1 signal path (
/rtc/v1) carries theJoinRequestprotobuf as a base64 query parameter, built inUtils.buildWrappedJoinRequest. Two things diverged from client-sdk-js and rust-sdks.1. Standard base64 instead of base64url
wrappedData.base64EncodedString()emits+and/.URLComponentspercent-encodes=but leaves+and/literal in the query:A receiver that parses the query as form-urlencoded decodes
+as a space, which corrupts the payload. Go'snet/urldoes exactly this. Both other SDKs avoid it deliberately:BASE64_URL_SAFE.encode(&wrapped_bytes)— "URL-safe base64 avoids percent-encoding issues in query parameters" (livekit-signaling/src/lib.rs:876-879).replace(/\+/g, '-').replace(/\//g, '_')(src/api/SignalClient.ts:1457)This has likely gone unnoticed because the v1 path only runs with
RoomOptions.singlePeerConnection = true, which isfalseby default in Swift. This is the part that needs a confirming run against a real server — the intent in the other two SDKs is unambiguous, but I have not reproduced the server-side corruption end to end. Padding is preserved, matching both other SDKs.2. Compression was hardcoded to
.noneThe request travels in the WebSocket upgrade URL, so an oversized request splits across TCP segments — on a lossy path one lost segment costs an RTO before the handshake begins. Rust always gzips and keeps the compressed form only when smaller (
lib.rs:853-871); JS gzips wheneverCompressionStreamexists (SignalClient.ts:1425-1450). This adopts the same rule.Apple's Compression framework has no gzip container —
COMPRESSION_ZLIBproduces a raw DEFLATE stream with no header, checksum or length trailer — soGzipwraps it in RFC 1952 framing with a table-driven CRC-32 rather than linking zlib solely for its container. The mtime field is left zero so encoding is deterministic and no clock value is disclosed.What this actually saves today
Measured with the same encoder on realistic payloads:
So there is no change on the wire today beyond the encoding fix: a minimal join request is smaller than gzip's framing, so the size comparison correctly declines to compress it (covered by
smallRequestIsNotCompressed). The payoff arrives with offer-with-join (CLT ticket 2), where the SDP makes the payload multi-KB — which is why JS gates offer-with-join on compression being available (isPublisherOfferWithJoinSupported = isCompressionStreamSupported() && !isFireFox()). Landing this first unblocks that work.Testing
New
JoinRequestUrlTests/GzipTests(8 tests, all passing):join_requestcontains no+or/in the percent-encoded queryWrappedJoinRequest→ gunzip →JoinRequest, for both fresh connect and quick reconnect.none"123456789"→0xCBF43926), and empty input →0nilBuilds verified on macOS, Mac Catalyst and iOS Simulator; tvOS/visionOS SDKs aren't installed locally, so CI covers those (the Compression framework is available on all of them).
swiftlintandswiftformat --lintclean. No public API change —Gzipand theDataextension are internal.🤖 Generated with Claude Code