Skip to content

add ConnectOptions::with_websocket_config for MqttClient (#165) - #210

Merged
fabracht merged 18 commits into
LabOverWire:mainfrom
bryan-ashby:client-websocket-config
Oct 11, 2026
Merged

fabracht merged 18 commits into
LabOverWire:mainfrom
bryan-ashby:client-websocket-config

Conversation

@bryan-ashby

Copy link
Copy Markdown
Contributor

Part of #165, as agreed there: a static WebSocket configuration on ConnectOptions, applied on every connect and reconnect. The request modifier will come separately.

Builds on #189 and #191: the headers only reach the wire with #189, and this edits the client's wss:// path that #191 rewrote. Until those merge, the diff includes their commits; the change here is the last commit.

Changes

  • ConnectOptions::with_websocket_config(WebSocketConfig), stored as Option<Arc<WebSocketConfig>> (behind transport-websocket). An Arc keeps the connect futures under clippy's large_futures limit.
  • MqttClient reads it on every connection attempt. connect_websocket and connect_websocket_secure read it from the client's current options, so automatic reconnects use it too. The URL is replaced by the address passed to connect (crate-private WebSocketConfig::with_url, which validates the scheme like new), and the headers, subprotocols, user agent and timeout are kept.
  • wss TLS precedence: a TLS config set on the WebSocketConfig is used as given, except that set_insecure_tls(true) still disables verification. Without one, the stored config from set_tls_config/connect_with_tls applies as in use WebSocketConfig's TLS configuration for wss:// connections (#190) #191.
  • WebSocketConfig implements Clone. Its Debug still redacts header values, and ConnectOptions' Debug now includes it.

Tests

tests/client_websocket_config.rs:

  • websocket_config_is_sent_on_connect_and_on_reconnect. A loopback ws:// server captures each upgrade request, answers CONNECT with a CONNACK, then closes the connection so the client reconnects. Both requests carry the configured Authorization, mqttv5.0, mqtt and User-Agent, and Host is the address passed to connect rather than the config's placeholder URL.
  • websocket_config_tls_is_used_for_wss. With no stored TLS config, the client only completes the wss handshake if the WebSocketConfig's CA is used.

Both fail without the client/inner.rs change.

Locally, on the pinned 1.98.0 toolchain, fmt, clippy (workspace, wasm, and the lib with no features or a single feature), cargo doc -D warnings and the doctests pass. The full suite passes apart from the same machine-only failures noted on #189 and #191. Fork CI: bryan-ashby#3.

🤖 Generated with Claude Code

bryan-ashby and others added 9 commits October 2, 2026 10:01
…ade request (LabOverWire#165)

WebSocketTransport::connect built the upgrade request from a fixed set of
headers and always offered Sec-WebSocket-Protocol: mqtt, so with_header,
with_subprotocol(s) and with_user_agent never reached the server.

Request construction moves to WebSocketConfig::build_handshake_request,
which starts from tungstenite's into_client_request (fixing a Host header
that dropped non-default ports) and adds the configured subprotocols, user
agent and custom headers. Invalid, reserved or case-duplicated headers and
non-token subprotocols are rejected before dialing. WebSocketConfig's Debug
no longer prints header values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LabOverWire#165)

Content-Length, Transfer-Encoding and Sec-WebSocket-Accept are now
rejected as custom handshake headers alongside the existing reserved
names. Custom headers are validated and sent in name order, so the
request and the reported error do not depend on HashMap iteration. The
integration test checks Host against the exact bound address, and the
CHANGELOG calls out the changes that can affect existing configurations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erWire#190)

With server-certificate verification on, WebSocketTransport::connect used
tokio-tungstenite's own client config built from the platform roots, so a
configured CA, client certificate, use_system_roots and ALPN protocols
were ignored. With verification off, the client certificate and ALPN were
dropped.

The rustls client config construction moves from TlsTransport to a public
TlsConfig::client_config, and a wss:// connection with a tls_config now
passes that config to tokio-tungstenite in both cases. Without a
tls_config, the platform roots are still used. The WSS module's duplicate
NoVerifier is replaced by the shared NoVerification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S config over wss (LabOverWire#190)

with_tls_auto parsed the URL's host and port as a socket address, so it
and the with_ca_cert_* and with_client_auth_* methods that call it failed
for any host name. A wss:// connection takes its server and certificate
name from the URL, so a host name now gets the unspecified address with
the URL's port.

MqttClient's wss:// path built its WebSocketConfig from the URL alone and
ignored the TLS configuration stored by set_tls_config; it now applies it
as connect_tls does. New tests cover a host name URL, the URL host being
verified rather than TlsConfig::hostname, and MqttClient presenting its
stored client certificate, and two rejection tests now assert
UnknownIssuer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…headers (LabOverWire#165)

Review follow-up. The configured subprotocols are now followed by "mqtt"
when the list does not include it, as MQTT-6.0.0-3 requires: the broker
in this crate only selects a subprotocol the client offers, so a config
preferring mqttv5.0 failed with "Server sent no subprotocol". A
subprotocol listed twice is rejected (RFC 6455 §4.1), and TE, Trailer,
Keep-Alive, Proxy-Connection and Expect join the reserved headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LabOverWire#190)

Review follow-up. With a tls_config and use_system_roots, wss:// now
loads the platform's root certificates through rustls-native-certs, as
tokio-tungstenite already did without a tls_config, instead of the
bundled webpki-roots, so a CA installed on the machine keeps working for
an ALPN-only config or MqttClient::set_tls_config(cert, key, None).
TlsTransport keeps webpki-roots; roots are only loaded when the server
certificate is verified.

MqttClient's wss:// path no longer offers the stored config's ALPN
protocols, which select MQTT directly over TLS; a WebSocketConfig's own
ALPN is still offered, and the CHANGELOG notes that a server advertising
only http/1.1 rejects an MQTT one. TlsConfig::addr and with_tls_auto
document the 0.0.0.0 placeholder, and the private key is copied with
clone_key() rather than an expect. The wss test server moves to a shared
module for the new SSL_CERT_FILE test binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#165)

MqttClient built each connection's WebSocketConfig from the URL alone, so
its ws:// and wss:// connections could not send custom handshake headers,
subprotocols or a user agent. ConnectOptions::with_websocket_config stores
a WebSocketConfig that the client reads on every connection attempt,
including automatic reconnects, with the URL replaced by the address
passed to connect. For wss://, its TLS configuration, when set, is used in
place of the stored one; insecure_tls still disables verification.

WebSocketConfig now implements Clone, and is held behind an Arc so the
connect futures stay small.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@fabracht fabracht left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, Bryan, this is what I had in mind. I ran an MqttClient with with_websocket_config against our ws listener, with a placeholder URL, mqttv5.0 and a custom header, and it connects, subscribes and gets its message. Your two tests fail without the client/inner.rs change, as you said.

I made the last few changes myself so we can get it in:

  • Merged main into your branch for the conflicts #189 and #191 left behind. Your code is unchanged.
  • ConnectOptions has public fields and no #[non_exhaustive], so the new field breaks code that builds it with a struct literal. I checked with a small crate outside the workspace: a literal that compiles against main fails here with missing field websocket_config, and only when transport-websocket is on. I added a Breaking entry for it to the CHANGELOG, like the one for resume_existing_session in 0.41.0, and it will go out in a minor release.
  • We run clippy with -W clippy::pedantic, and with the bigger ConnectOptions it flagged nine test calls as large futures, in quic_multistream_integration.rs, broker_quic_integration.rs and clean_start_prebind_subscribe.rs. They're wrapped in Box::pin now.
  • parse_websocket_url had landed between is_http_token and its doc comment, so I moved the comment back.

Nothing left for you to do here, merging once CI passes.

@fabracht
fabracht merged commit f2d95bc into LabOverWire:main Oct 11, 2026
11 checks passed
@fabracht

Copy link
Copy Markdown
Contributor

Thanks Bryan, this is merged now.

We added one thing before merging. The client follows a CONNACK redirect (UseAnotherServer or ServerMoved) on its own, and the redirected connection was reusing the WebSocket config, so the custom headers (an Authorization header, for example) went to whatever server the redirect named. Now only the address passed to connect gets the config, and a redirected connection uses the defaults. There's a test for it in client_websocket_config.rs, and the doc comment and CHANGELOG entry say so too. We also took out the inline comments to match the rest of the code.

The red Rust CI run on your fork came from a timing test of ours that counted backend setup time as lock time. That's fixed on main in #216 and has nothing to do with your change.

It'll go out in the next release.

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