Skip to content

feat: add TURN-TCP support with server-driven config#116

Open
nagar-decart wants to merge 11 commits intomainfrom
nagar-decart/sdk-turn-tcp
Open

feat: add TURN-TCP support with server-driven config#116
nagar-decart wants to merge 11 commits intomainfrom
nagar-decart/sdk-turn-tcp

Conversation

@nagar-decart
Copy link
Copy Markdown
Contributor

@nagar-decart nagar-decart commented Apr 7, 2026

Summary

  • Add TURN-over-TCP support for WebRTC fallback in restricted networks where UDP is blocked
  • SDK accepts server-driven turn_config via WebSocket signaling, giving full server-side control over TURN rollout
  • Add iceServers option to connect() for manual/internal testing without server-side changes
  • Add iceTransportPolicy option (tcp/udp/all) forwarded as query param for per-session transport control
  • Phase 2.5 wait for turn_config only when TURN is requested (zero latency impact on normal connections)
  • Add Playwright E2E tests and vitest config for local k8s validation

API side: DecartAI/api#971

Changes

  • webrtc-connection.ts: Handle turn_config message, Phase 2.5 wait, merge server-driven + explicit ICE servers
  • webrtc-manager.ts / client.ts: Thread iceServers, iceTransportPolicy, expectTurnConfig options
  • types.ts: Add TurnConfigMessage type
  • e2e-turn-tcp.test.ts: E2E tests for relay-only and dual-mode paths
  • vitest.config.e2e-turn-tcp.ts: Vitest config for TURN-TCP tests

Test plan

  • pnpm typecheck clean
  • pnpm build succeeds
  • pnpm test — 175 unit tests pass
  • pnpm 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_config over WebSocket signaling and by letting callers optionally provide custom iceServers.

realtime.connect() now accepts iceTransportPolicy (tcp/udp/all) and rtpPort, forwards them as query params, and (when TURN is requested) briefly waits for turn_config before creating the RTCPeerConnection; 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-tcp suite/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.

nagar-decart and others added 7 commits April 7, 2026 13:54
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>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f1f721. Configure here.

connectAbort,
]);
if (turnHandler) this.websocketMessagesEmitter.off("turnConfig", turnHandler);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f1f721. Configure here.

nagar-decart and others added 4 commits April 9, 2026 09:41
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>
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.

1 participant