feat: add TURN-TCP support with server-driven config#116
feat: add TURN-TCP support with server-driven config#116nagar-decart wants to merge 11 commits intomainfrom
Conversation
Add TURN-over-TCP ICE server alongside existing STUN for networks where UDP is blocked. ICE naturally prefers direct UDP and falls back to TURN-TCP relay via coturn. Includes Playwright E2E tests verifying both relay-only and dual-mode paths against local k8s slim-bit-invert. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Handle `turn_config` WebSocket message from server to receive TURN servers dynamically (for production server-side control) - Add `iceServers` option to connect() for manual/internal testing - Remove hardcoded TURN from ICE_SERVERS (now server-driven or explicit) - Skip relay-only E2E test until server-side aioice TURN allocation is verified Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the server sends turn_config after the PeerConnection is already created (race: bouncer acks Phase 2 locally before upstream pump delivers turn_config), update the PC's ICE servers via setConfiguration() and trigger an ICE restart so TURN candidates are gathered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When turn_config triggers an ICE restart, the answer to the original offer may arrive after the new offer resets signaling state to stable. Guard setRemoteDescription(answer) with a signalingState check to drop stale answers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace ICE restart approach with a Phase 2.5 wait: after Phase 2 completes, wait up to 2s for turn_config to arrive before creating the PeerConnection. This ensures TURN servers are included from the start — one offer, one answer, no re-negotiation. Also reverts the stale answer guard (no longer needed without ICE restart). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Forwards ice_transport_policy query param to the server, which filters
TURN URLs by transport (tcp/udp/all) and configures server-side ICE
accordingly.
Usage: client.realtime.connect(stream, { model, iceTransportPolicy: "tcp" })
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Only wait for turn_config in Phase 2.5 when iceTransportPolicy is set (server expected to send TURN config). Connections without TURN have zero added latency. - Clean up turnConfig event handler after Promise.race resolves, preventing leaked handlers on reconnection cycles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1f1f721. Configure here.
| initialImage, | ||
| initialPrompt, | ||
| iceServers: options.iceServers, | ||
| expectTurnConfig: !!options.iceTransportPolicy, |
There was a problem hiding this comment.
Unnecessary 2-second wait when iceTransportPolicy is "udp"
Medium Severity
expectTurnConfig: !!options.iceTransportPolicy evaluates to true for all policy values, including "udp". When iceTransportPolicy is "udp", the server is unlikely to send a turn_config message (TURN is a TCP/relay fallback), so Phase 2.5 always hits the full 2-second timeout before proceeding. The condition needs to exclude "udp" to match the stated goal of "zero latency impact" when TURN isn't requested.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 1f1f721. Configure here.
| connectAbort, | ||
| ]); | ||
| if (turnHandler) this.websocketMessagesEmitter.off("turnConfig", turnHandler); | ||
| } |
There was a problem hiding this comment.
Event handler leak when connectAbort fires during Phase 2.5
Low Severity
If connectAbort rejects during the Phase 2.5 Promise.race, the await throws and the turnHandler cleanup on line 180 is skipped. The handler remains registered on websocketMessagesEmitter, which isn't cleared by cleanup(). This leaks one handler per failed connection attempt when TURN config is expected.
Reviewed by Cursor Bugbot for commit 1f1f721. Configure here.
Set iceTransportPolicy: "relay" on the RTCPeerConnection when expectTurnConfig is true, so the browser only uses TURN relay candidates and doesn't fall back to direct connections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
"all" means both direct and TURN candidates should be available — ICE picks the best path. Only "tcp" and "udp" should force iceTransportPolicy: "relay" on the PeerConnection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Optional params forwarded as rtp_port_min/rtp_port_max query params to let clients specify a custom UDP port range for WebRTC sessions behind restrictive firewalls. Server-side support in DecartAI/api#987. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>


Summary
turn_configvia WebSocket signaling, giving full server-side control over TURN rollouticeServersoption toconnect()for manual/internal testing without server-side changesiceTransportPolicyoption (tcp/udp/all) forwarded as query param for per-session transport controlturn_configonly when TURN is requested (zero latency impact on normal connections)API side: DecartAI/api#971
Changes
webrtc-connection.ts: Handleturn_configmessage, Phase 2.5 wait, merge server-driven + explicit ICE serverswebrtc-manager.ts/client.ts: ThreadiceServers,iceTransportPolicy,expectTurnConfigoptionstypes.ts: AddTurnConfigMessagetypee2e-turn-tcp.test.ts: E2E tests for relay-only and dual-mode pathsvitest.config.e2e-turn-tcp.ts: Vitest config for TURN-TCP testsTest plan
pnpm typecheckcleanpnpm buildsucceedspnpm test— 175 unit tests passpnpm test:e2e:turn-tcp— dual-mode test passes against local k8s🤖 Generated with Claude Code
Note
Medium Risk
Touches WebRTC signaling/ICE setup and connection sequencing, which can affect connectivity and reconnection behavior across environments. Changes are gated behind new options/defaults, but misconfiguration or server message timing could still cause connection failures.
Overview
Adds TURN/TURN-TCP support to the realtime SDK by allowing the server to push
turn_configover WebSocket signaling and by letting callers optionally provide customiceServers.realtime.connect()now acceptsiceTransportPolicy(tcp/udp/all) andrtpPort, forwards them as query params, and (when TURN is requested) briefly waits forturn_configbefore creating theRTCPeerConnection; ICE servers are merged from defaults, caller-provided config, and server-provided TURN credentials, with optional relay-only enforcement.Includes a new Playwright-based
test:e2e:turn-tcpsuite/config and excludes it from the default unit test run.Reviewed by Cursor Bugbot for commit 14d25b3. Bugbot is set up for automated code reviews on this repo. Configure here.