Skip to content

fix(http)!: honor custom configuration with safe WebSocket handshakes - #333

Open
SebTardif wants to merge 2 commits into
agentclientprotocol:mainfrom
SebTardif:fix/http-ws-honor-custom-client
Open

fix(http)!: honor custom configuration with safe WebSocket handshakes#333
SebTardif wants to merge 2 commits into
agentclientprotocol:mainfrom
SebTardif:fix/http-ws-honor-custom-client

Conversation

@SebTardif

@SebTardif SebTardif commented Aug 23, 2026

Copy link
Copy Markdown

Summary

The WebSocket path previously discarded the configured reqwest client and called tungstenite with only the URL. This change uses reqwest for the opening handshake so headers, timeouts, proxies, DNS, and TLS configuration are honored, then uses the existing async-tungstenite stack for framing.

Client construction now goes through an SDK builder so WebSocket connection policies can be applied before the underlying reqwest client is built:

let transport = HttpClient::builder("wss://agent.example")
    .configure_http(|http| {
        http.default_headers(headers)
            .connect_timeout(Duration::from_secs(5))
            .timeout(Duration::from_secs(10))
    })
    .build()?;
  • builder(base_url) retains the /acp normalization rule; builder_with_endpoint(endpoint) preserves an exact endpoint.
  • WebSocket clients use HTTP/1.1 and do not follow redirects, even if the configuration callback selected another policy.
  • HTTP/SSE retains the caller's HTTP-version and redirect policies.
  • The complete handshake is validated before any queued ACP data is sent: HTTP version/status, Upgrade, Connection tokens, unique Accept, and rejection of unsupported subprotocols/extensions.
  • from_http_client(exact_endpoint, client) supports reuse of an existing HTTP/SSE client and its connection pool.
  • new and with_endpoint remain available unchanged; HttpClient can be cloned to share its underlying HTTP client.

Compatibility and migration

BREAKING CHANGE: WebSocket URLs passed to constructors taking an already-built reqwest client now return HttpClientError::WebSocketRequiresBuilder before network I/O. An opaque, prebuilt client cannot be reconfigured to enforce the WebSocket connection policies. Use the builder API above instead.

with_client and with_endpoint_and_client are retained as deprecated compatibility wrappers, not removed. Existing HTTP/SSE calls preserve their path handling, configured client, and connection pool. They can migrate incrementally to the builder or from_http_client; the latter takes an exact endpoint and does not append /acp.

Normal reqwest TLS options support custom trust roots and client identities. A raw tls_backend_preconfigured backend must itself select HTTP/1.1 ALPN: reqwest does not rewrite preconfigured TLS ALPN. Incompatible negotiation fails before ACP data is sent.

Request/read timeouts cover the opening WebSocket handshake, not the lifetime of the upgraded socket. HTTP/SSE keeps normal reqwest request/body timeout semantics.

The book, rustdoc, README, and changelog include configuration and migration guidance.

Verification

  • just test — full workspace suite passed.
  • cargo fmt --all -- --check — passed.
  • HTTP crate Clippy with all targets/features and -D warnings — passed.
  • HTTP crate all-targets/all-features check on Rust 1.88.0 — passed.
  • Client-only, server-only, and no-feature library checks — passed.
  • mdbook build and HTTP crate rustdoc with -D warnings — passed.

Regression coverage includes malformed handshakes with zero ACP writes, deprecated-constructor migration behavior, default headers, separate request/read handshake timeouts, custom trust roots and HTTP/2 feature unification, raw TLS ALPN compatibility, redirects that never reach their destination (including WSS-to-HTTP downgrade), authenticated CONNECT proxy tunnels, upgraded-stream timeout lifetime, and preservation of HTTP/SSE redirect and HTTP/2 policies. TLS certificates are generated in memory at test runtime; no private key fixtures are committed.

SebTardif and others added 2 commits August 23, 2026 11:36
run_ws discarded the configured Client and called
async-tungstenite connect_async with only the URL. HttpClient::with_client
and with_endpoint_and_client therefore ignored timeout, default headers,
proxy, and TLS on ws:// and wss://.

Perform the handshake with the reqwest Client, then wrap the upgraded
stream as a tungstenite WebSocket.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Build configured transports through HttpClientBuilder so WebSocket handshakes use HTTP/1.1 and cannot follow redirects. Validate the complete response before sending ACP data while preserving HTTP/SSE configuration and reusable clients.

Keep with_client and with_endpoint_and_client as deprecated HTTP/SSE wrappers. Cover TLS ALPN, proxies, redirects, timeouts, malformed handshakes, feature unification, and migration behavior.

BREAKING CHANGE: WebSocket URLs passed to constructors taking a prebuilt reqwest Client now return WebSocketRequiresBuilder before network I/O. Use builder or builder_with_endpoint with configure_http instead.
@benbrandt benbrandt changed the title fix(http): honor custom reqwest Client on WebSocket connect fix(http)!: honor custom configuration with safe WebSocket handshakes Sep 11, 2026
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