Repository navigation
add WebSocketConfig::with_request_modifier (#165) - #211
Merged
fabracht merged 16 commits intoOct 11, 2026
Merged
Conversation
…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>
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
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>
An async callback that receives the validated upgrade request and returns the one to send, so values computed at connect time, such as a short-lived signed token or a presigned URI, can be added to each connection attempt. Through ConnectOptions::with_websocket_config it runs before every MqttClient attempt, including automatic reconnects. It runs within the connect timeout, and an error fails the attempt with ConnectionError before dialing. The handshake future is boxed so the connect futures that await it stay small. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
Contributor
|
Thanks Bryan, this is merged now. The modifier is a good fit for the presigned URL and short-lived token cases. We made a few changes before merging:
There are tests for each of these. It'll go out in the next release. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #165: the request modifier, the second half of what we agreed there (the static config is #210).
Builds on #210, which builds on #189 and #191. Until those merge, the diff includes their commits; the change here is the last commit.
Changes
WebSocketConfig::with_request_modifiertakes an async callback:build_handshake_requestand returns the request to send. Use cases: a short-lived signed token (e.g. an AWS IoT custom-authorizer signature), or a presigned URI.WebSocketConfiggiven toConnectOptions::with_websocket_config, it runs before everyMqttClientconnection attempt, including automatic reconnects, because the client reads that config on each attempt (add ConnectOptions::with_websocket_config for MqttClient (#165) #210). So the values never go stale.MqttError::ConnectionError("WebSocket request modifier failed: …")before anything is dialed, so a reconnecting client retries as it would after any connect failure.with_headerchecks don't cover headers the callback adds, and tungstenite still requires its handshake headers exactly once. The connection goes to the returned request's URI; the docs note that a callback pointing it at another host must also updateHost.RequestModifier(Clone, with aDebugthat printsRequestModifier(..)).WebSocketConfiggains arequest_modifierfield, andRequestModifierErroris the boxed error type.large_futureslimit.Tests
tests/websocket_request_modifier.rs, against a loopback server:modifier_adds_headers_computed_at_connect_timemodifier_can_replace_the_urimodifier_error_fails_connect_before_dialingConnectionErrorwith the callback's message; the server sees no connectionmodifier_runs_within_the_connect_timeoutMqttError::TimeoutIn
tests/client_websocket_config.rs,request_modifier_runs_again_on_reconnectuses #210's server, which closes each connection after CONNACK. It checks that the first connect carriesx-token: token-1and the automatic reconnect carriestoken-2.All five fail if
connectignores the modifier. The two #210 tests in that file still pass, as expected.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 warningsand the doctests pass. The full suite passes apart from the same machine-only failures noted on #189 and #191. Fork CI: bryan-ashby#4.🤖 Generated with Claude Code