fix(sdk-rust): share one TLS config across WebSocket connects

Merged
WI
5d ago
+202-3

The Rust SDK's WebSocket client builds its rustls ClientConfig from the platform's native roots once per process and hands it to every wss:// connect and reconnect in ws.rs.

How it works

  • ws_tls::connect (private) wraps connect_async_tls_with_config with Connector::Rustls(shared config) for wss://; ws:// connects never read roots.
  • The config sits behind a tokio::sync::Mutex, so concurrent first connects wait for a single load instead of each reading the store. The load runs on the blocking pool (spawn_blocking), off the runtime's workers.
  • An empty root store fails that connect with io::ErrorKind::NotFound (the same "no native root CA certificates found" error tokio-tungstenite returned) and is not cached, so the next connect or reconnect reads the store again.
  • rustls and rustls-native-certs are direct dependencies at the versions tokio-tungstenite already links; no new crypto provider or feature is enabled.

Public API is unchanged.

Before / after (macOS, arm64)

Release build of a scratch binary that calls WsClient::connect() 20 times against a local wss://127.0.0.1 listener that drops each socket, so every connect reaches TLS setup and fails:

first connecteach later connectprocess CPU (user+sys)instructions retired
origin/main128 ms120 ms1.54 s21.9 B
this branch128 ms0.1 ms0.08 s1.2 B

A 3 s sample of the same loop running continuously: on origin/main 2,486 samples sit under rustls_native_certs::macos::load_native_certs (the keychain enumeration and per-certificate trust evaluation); on this branch there are none.

Unit tests in src/ws_tls.rs:

  • concurrent_first_connects_read_the_roots_once: 8 tasks on a 4-worker runtime race the first connect; the loader runs once and every task gets the same Arc<ClientConfig>.
  • no_roots_fail_the_connect_and_are_asked_for_again: an empty store errors with NotFound; the next call loads again and its config is then reused (2 loads for 3 calls).
  • reconnects_reuse_the_first_connects_configuration: two real wss:// connects through connect_with (the body of connect) with a fixed root store load once and share one config. None of the tests reads the host's certificates.

Other SDKs

None reads a root store per connect:

  • TypeScript uses the runtime's global WebSocket (Node/undici or the browser), which holds its CA store per process.
  • Swift uses URLSession.webSocketTask; trust evaluation is the system's, with no SDK-side store load.
  • Python: the SDK was removed in #589.

Semver

This change is a patch on its own: an internal performance fix with no public API change. It touches only the relaycast crate, so no npm package (the lockstep v12.1.0 line) needs a release for it.

The crate's next crates.io version is 11.0.0 (major), because its baseline is the crate's own version line, not v12.1.0:

  • crates.io relaycast 10.0.0 was published from 095a346e (its src/ matches that commit exactly) and still has pub plan: String on Workspace.
  • #590 removed Workspace::plan from the crate after that publish. That change is inside v12.1.0, and main equals v12.1.0, but the crate itself has not been released since 10.0.0 (Cargo.toml still reads 10.0.0; the latest sdk-rust-v* tag is sdk-rust-v9.0.1).
  • Dispatching publish-rust.yml with patch from main would ship that field removal as 10.0.1. Dispatch with major → 11.0.0. The workflow rebases its release commit onto main and pushes it there, so it does not support a 10.0.1 cut from a side branch.
  • relay-desktop reads info.plan (probe/src/relay/client.rs:47, probe/src/relay/daemon/workspace_verification.rs:71), so its bump to 11.0.0 drops those reads.

🤖 Generated with Claude Code


Agent Relay sessions

  • claude session 38a7c19b · opened via gh pr create · last active 2026-10-28