Skip to content

feat(eventhubs): support AMQP-over-WebSockets transport - #4596

Closed
Johnathan W (j7nw4r) wants to merge 25 commits into
Azure:mainfrom
j7nw4r:worktree-eventhubs-amqp-websockets
Closed

feat(eventhubs): support AMQP-over-WebSockets transport#4596
Johnathan W (j7nw4r) wants to merge 25 commits into
Azure:mainfrom
j7nw4r:worktree-eventhubs-amqp-websockets

Conversation

@j7nw4r

@j7nw4r Johnathan W (j7nw4r) commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

azure_messaging_eventhubs connected over AMQP on TCP/TLS only (port 5671). No option selected AMQP-over-WebSockets (wss://, port 443). Some networks block the native AMQP ports (5671 and 5672). Clients on those networks had no path to Event Hubs. The .NET, Java, and Python Azure SDKs all offer this option. (#3601)

This change adds a transport option, and it keeps the TLS stack a separate choice. Both parts are additive. No existing caller breaks, and Tcp stays the default.

Motivation

The AMQP WebSocket binding sends an AMQP connection over a WebSocket on port 443. The service treats that connection as equal to a 5671 connection. The client builder and the AMQP backend fixed the scheme to amqps:// and the transport to TCP, so no public option reached the WebSocket binding. Some firewalls block every outbound port except 443. Clients behind those firewalls cannot reach Event Hubs without a transport selector.

A WebSocket connection also needs a TLS stack. Most applications already link one stack. A second stack adds binary size, and it adds a second trust store to configure. azure_core keeps the transport and the stack apart for HTTP, where reqwest is the base feature and reqwest_rustls adds the stack. A consumer of the AMQP crates expects the same shape.

The TLS stack also controls which root certificates the client trusts. A change of stack must keep the anchors that the previous stack used, or a broker behind a private certificate authority stops working.

Changes

  • azure_core_amqp adds the public AmqpTransport { Tcp, WebSocket } enum and an AmqpConnectionOptions::transport field. AmqpConnectionOptions stays a plain options bag with public fields, so ..Default::default() still builds it. The open() function of the fe2o3 backend branches on the transport. The WebSocket arm connects a fe2o3_amqp_ws::WebSocketStream, and it gives the stream to Connection::builder().open_with_stream(...). The AMQP hostname stays the real service host, so AMQP link addressing does not change. SASL ANONYMOUS and CBS authorization stay the same.
  • The WebSocket arm calls connect_with_config, not connect_tls_with_config. fe2o3-amqp-ws puts connect_tls_with_config behind its own TLS features, and a call to it would keep fe2o3_amqp_ws from building on its own. connect_with_config carries no such gate. It uses whichever stack the features of fe2o3-amqp-ws select, and the connection reports TlsFeatureNotEnabled when the graph selects none.
  • The crate offers four features. fe2o3_amqp is the base backend. fe2o3_amqp_rustls adds the TLS stack for AMQP framed on TCP. fe2o3_amqp_ws turns on the WebSocket transport code and the fe2o3-amqp-ws dependency, and it names no TLS stack. fe2o3_amqp_ws_rustls adds the stack for the WebSocket transport. default selects all four. Each stack is rustls with the aws-lc-rs provider, which is the stack that the rest of sdk/core uses.
  • The TCP transport supplies its own TLS connector, which fe2o3_amqp_rustls builds on rustls-platform-verifier. The default connector of fe2o3-amqp fills its root store from webpki-roots, which is a compiled-in copy of the Mozilla root set, and it ignores the trust store of the operating system. native-tls read that store before this change, so the default would drop the roots that an operator installs, and a broker behind a private or an enterprise certificate authority would fail the handshake. The platform verifier reads the trust store of the operating system on every supported target, through Security.framework on Apple, through CryptoAPI on Windows, and through rustls-native-certs elsewhere. The TCP transport therefore keeps the anchors it had, and it agrees with reqwest/rustls on the HTTP side and with the WebSocket transport, which reads the same store through native roots. webpki-roots still arrives as a dependency, because the rustls feature of fe2o3-amqp always names it, and nothing reads it.
  • The connector reaches the builder through rustls_connector, not through the tls_connector alias. rustls_connector needs the rustls feature only, where the alias also needs native-tls to be off, so the call survives a consumer that unifies both features. The connector-typed builder keeps its own open, and that path hands alt_tls_estab to the same transport call as the default path, so alt_tls_establishment(true) keeps its behavior.
  • There is no native-tls feature. fe2o3-amqp accepts one TLS stack only, and it reports TlsConnectorNotFound at run time when both are on. Two features that cannot both be on would break --all-features. To build on another stack, turn off the default features, name the base features, and take a direct dependency on fe2o3-amqp and fe2o3-amqp-ws with the stack you want. Cargo unifies the features, and nothing pulls rustls in. samples/list_blobs_native_tls shows the same pattern for reqwest. The feature comments record this recipe.
  • fe2o3_amqp_rustls and fe2o3_amqp_ws_rustls each take a direct rustls dependency. That dependency only selects the crypto provider, and this crate names one rustls type, which is the ClientConfig that the connector needs. The tokio-tungstenite chain under fe2o3-amqp-ws takes rustls with default-features = false and turns on no provider feature, so ClientConfig::builder() panics when no process-level default is installed. The TCP connector calls the same builder and needs the same guarantee. The default features of rustls give aws-lc-rs, std, and tls12, and deny.toml bans ring, so this repository cannot reach that panic. An application that unifies a second provider into the graph must call CryptoProvider::install_default() first.
  • The workspace moves the fe2o3-amqp family from 0.14 to 0.16, and it adds fe2o3-amqp-ws 0.16, rustls 0.23, rustls-platform-verifier 0.7, and tokio-rustls 0.26. The rustls backend of fe2o3-amqp 0.14 was built on ring, which deny.toml bans. Version 0.16 uses aws-lc-rs instead, so this change closes Cannot use aws-lc-rs for AMQP #4189. reqwest already resolves rustls-platform-verifier and tokio-rustls through its own rustls feature, so the graph gains no new package, and Cargo.lock gains two dependency edges only.
  • azure_messaging_eventhubs forwards all four features. azure_core forwards the reqwest TLS features of typespec_client_core in the same way. An application depends on the Event Hubs crate, so the split does not reach the application without the forward.
  • azure_messaging_eventhubs re-exports AmqpTransport as models::AmqpTransport, next to the other azure_core_amqp types that this crate already re-exports. It adds with_transport(...) on the producer builder and the consumer builder. The transport reaches the connection on all four construction paths, which are open and open_with_connection_string on each of the two builders.
  • EventProcessor takes the transport from the ConsumerClient that build receives. It therefore runs over WebSockets when that client selects them. The documentation on build shows this with an example.
  • The WebSocket address is wss://{host}[:{port}]/$servicebus/websocket/. The other Azure SDKs use the same binding path. A custom endpoint redirects the socket, and the AMQP hostname stays the real service host.
  • The eventhubs_websocket_transport sample opens a producer and a consumer over WebSockets, sends a tagged event, and reads the event back.
  • The docs.rs metadata names all four features, so docs.rs documents both transports together with their TLS stacks.
  • azure_messaging_eventhubs needs the unreleased AmqpTransport. It therefore takes azure_core_amqp and azure_core as local path plus version dependencies, and it takes the local dev-dependencies as path only. This follows the policy in AGENTS.md. azure_messaging_eventhubs_checkpointstore_blob gets the same treatment. Without those lines, the registry build of azure_storage_blob pins the previous azure_core, and two copies of the crate land in the graph. Two copies break the trait bounds that cross between the two crates. These lines return to workspace = true when azure_core 1.2.0 publishes.

Related work

#4873 adds BufferedProducerClient, which gives the crate a third public builder. BufferedProducerClientBuilder forwards application_id, retry_options, and custom_endpoint to ProducerClient::builder(). It needs the same forward for the transport that this change adds. Without that one method, a buffered producer cannot use AMQP over WebSockets. Neither change blocks the other. The second change to merge adds the method.

Both changes edit producer/mod.rs, common/recoverable/connection.rs, and the changelog. The second change to merge therefore needs a small rebase. #4895 also edits common/recoverable/connection.rs.

Test plan

Five feature combinations of azure_core_amqp build, and each build passes --no-default-features. The feature sets are fe2o3_amqp; fe2o3_amqp with fe2o3_amqp_rustls; fe2o3_amqp with fe2o3_amqp_ws; fe2o3_amqp with fe2o3_amqp_ws and fe2o3_amqp_ws_rustls; and all four together. The default build passes, and --all-features --all-targets passes. The base-only build names fe2o3_amqp_ws with no TLS modifier, which is the combination that must compile without a stack.

The unit tests pass. azure_core_amqp runs 122 tests. azure_messaging_eventhubs runs 148 tests, and it ignores 14 live tests. 51 doc tests pass, 4 in the first crate and 47 in the second. The tests cover the WebSocket address construction, and they include an IPv6 case. They also cover the transport that reaches RecoverableConnection, the options that AmqpConnection::open receives, and the builder helper that every open path reads. One test builds the TCP connector, which reports a missing crypto provider at test time instead of at connect time.

cargo fmt --check passes, and cargo clippy --all-features --all-targets passes. cspell passes against the repository configuration. cargo tree shows no ring in the default graph.

A live run against an Event Hubs namespace validates both transports. Three connection-string tests passed over TCP, which are test_round_trip_connection_string, send_eventdata_with_connection_string, and consumer_open_with_connection_string. A temporary test then opened a producer on each transport, read the hub properties, and sent an event. Both transports passed. The eventhubs_websocket_transport sample opened a producer and a consumer with AmqpTransport::WebSocket, sent a tagged event, and read the event back. The process held one established socket during that run, to port 443. The eventhubs_connection_string sample keeps the default transport, and it ran against the same namespace as a control. That process held its socket on port 5671.

A negative control proves that the supplied connector governs the handshake. The service certificate chains to a public authority, and both root sets carry that authority, so a passing test alone does not separate the new connector from the default connector. A temporary build replaced the platform verifier with an empty root store. The live round trip then failed with invalid peer certificate: UnknownIssuer, and it passed again after the revert.

A run from a network that blocks 5671 outbound is still open. A broker behind a private certificate authority has the negative control only, because the test namespace uses a public authority. An application that selects another TLS stack has build coverage only.

@github-actions github-actions Bot added Azure.Core The azure_core crate Event Hubs labels Jun 15, 2026
@j7nw4r
Johnathan W (j7nw4r) force-pushed the worktree-eventhubs-amqp-websockets branch from 3ebc11b to 114e08b Compare July 20, 2026 18:49
@j7nw4r

Copy link
Copy Markdown
Member Author

Rebased onto main and re-pushed. Heads up on one thing I had to touch that isn't really about WebSockets, Heath Stewart (@heaths).

To see the unreleased AmqpTransport, eventhubs has to path-couple to azure_core_amqp, which after #4801 pulls the unreleased azure_core 1.2.0-beta.1. That cascades: azure_messaging_eventhubs_checkpointstore_blob implements an eventhubs trait, so it needs the same azure_core, but the registry build of azure_storage_blob hands it the older one, and both cross in checkpoint_store.rs. So I put azure_storage_blob back on path plus version, which is the line #4801 deliberately removed. If you'd rather keep checkpointstore packaging against the released blob crate, say so and I'll look for another way. All of it flips back to workspace = true once azure_core 1.2.0 ships.

Also folded in two fixes the rebase surfaced: a test in authorizer.rs added by #4587 needed the new RecoverableConnection::new arity, and ProducerClient::new crossed the too_many_arguments threshold at 8 args, which fails CI under -Dwarnings. I used an allow with the azure_identity precedent rather than growing an options struct mid-rebase; happy to do the struct as a follow-up if you'd prefer it.

Still no live round-trip over wss://. I don't have a namespace plus a 5671-blocked network to test from.

@j7nw4r

Copy link
Copy Markdown
Member Author

Validated this live against a real namespace, so the transport is no longer only unit-tested.

I added an eventhubs_websocket_transport sample that mirrors the connection-string one: open a producer and consumer with TransportType::AmqpWebSocket, send a tagged event, read it back. It passed. While it ran, the process held exactly one established socket, on port 443. I ran the default-transport sample against the same namespace as a control and it sat on 5671, so the selector is doing what it claims and not silently falling back.

Still outstanding is a run from a network that actually blocks 5671 outbound, which is the case this feature exists for. I do not have one handy.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
2 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI 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.

Pull request overview

Adds AMQP-over-WebSockets support for Event Hubs, enabling connections over port 443.

Changes:

  • Public API: adds transport selection to AMQP and Event Hubs clients.
  • Implements secure WebSocket transport in the fe2o3 backend.
  • Adds documentation, tests, example, and dependencies.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Cargo.toml Adds WebSocket dependency.
Cargo.lock Updates resolved dependencies.
sdk/core/azure_core_amqp/Cargo.toml Enables WebSocket support.
sdk/core/azure_core_amqp/CHANGELOG.md Documents the AMQP API.
sdk/core/azure_core_amqp/src/lib.rs Exports AmqpTransport.
sdk/core/azure_core_amqp/src/connection.rs Adds transport configuration.
sdk/core/azure_core_amqp/src/fe2o3/connection.rs Opens AMQP WebSocket streams.
sdk/core/azure_core_amqp/src/fe2o3/error.rs Maps WebSocket errors.
sdk/eventhubs/azure_messaging_eventhubs/Cargo.toml Uses unreleased local dependencies.
sdk/eventhubs/azure_messaging_eventhubs/CHANGELOG.md Documents transport selection.
sdk/eventhubs/azure_messaging_eventhubs/examples/eventhubs_websocket_transport.rs Demonstrates WebSocket transport.
sdk/eventhubs/azure_messaging_eventhubs/src/common/authorizer.rs Updates test construction.
sdk/eventhubs/azure_messaging_eventhubs/src/common/recoverable/connection.rs Propagates transport during recovery.
sdk/eventhubs/azure_messaging_eventhubs/src/consumer/mod.rs Adds consumer transport selection.
sdk/eventhubs/azure_messaging_eventhubs/src/event_processor/processor.rs Documents inherited transport.
sdk/eventhubs/azure_messaging_eventhubs/src/models/mod.rs Adds TransportType.
sdk/eventhubs/azure_messaging_eventhubs/src/producer/mod.rs Adds producer transport selection.
sdk/eventhubs/azure_messaging_eventhubs_checkpointstore_blob/Cargo.toml Aligns local dependency versions.

Comment thread sdk/core/azure_core_amqp/src/connection.rs
Comment thread sdk/core/azure_core_amqp/Cargo.toml Outdated
Comment thread sdk/core/azure_core_amqp/src/fe2o3/connection.rs
Comment thread Cargo.toml Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

sdk/core/azure_core_amqp/Cargo.toml:47

  • The fe2o3_amqp feature still enables fe2o3-amqp-ws without any TLS backend, but connection.rs unconditionally calls connect_tls_with_config, which is absent unless a WebSocket TLS feature is selected. As confirmed in the prior thread, cargo check -p azure_core_amqp --no-default-features --features fe2o3_amqp therefore fails, although this supported feature combination compiled before the change. Please either model shared TLS backend features for both fe2o3 crates and gate the WebSocket arm, or otherwise ensure this feature combination remains compilable.
  "dep:fe2o3-amqp-ws",

Copilot AI review requested due to automatic review settings July 21, 2026 16:45

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Comment thread sdk/core/azure_core_amqp/CHANGELOG.md Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 17:17
Add a runnable sample that opens a producer and a consumer with
TransportType::AmqpWebSocket, sends a tagged event, and reads it back.
It mirrors the connection-string sample, so the two can be run side by
side to compare transports.
Add an IPv6 regression test for the WebSocket address builder, register
fe2o3-amqp-ws in the crate spelling dictionary in both the underscore and
the hyphen form, and record the AmqpConnectionOptions field addition
under Breaking Changes.
Add the native_tls and rustls features, which select the TLS backend for
both fe2o3-amqp and fe2o3-amqp-ws. The default feature selects native_tls.
The WebSocket transport needs one of them, because fe2o3-amqp-ws gates its
TLS entry point behind its own features. Without a backend the crate now
compiles and the transport returns an error at run time. Before this
change, "--no-default-features --features fe2o3_amqp" did not compile.

Mark AmqpConnectionOptions as non_exhaustive and add with_* methods for
each field, so later fields are additive. Update the Event Hubs call site
to use the new methods.
The rustls backend of fe2o3-amqp depends on ring, and deny.toml bans that
crate, so "cargo deny --all-features check bans" failed. Keep native_tls
as the only TLS backend and gate the WebSocket transport on it. See issue
Azure#4189 for the ring work.
Extract RecoverableConnection::connection_options so a test can assert the
transport reaches the options given to AmqpConnection::open. A test on the
constructor alone passed even when create_connection dropped the call.

Route each builder open path through one transport() helper and test it,
which covers the connection-string path too.

Correct the CHANGELOG example: "..Default::default()" does not build a
non_exhaustive struct from another crate. Show the with_ method instead.
Document on AmqpTransport::WebSocket, on with_transport, and on the Event
Hubs TransportType::AmqpWebSocket that the variant needs a TLS backend.
A build with default-features = false can select the variant, but the
connection then returns an error when it opens.
The Event Hubs crate depends on azure_core_amqp with its default features,
so native_tls is always on and the WebSocket variant works in every build
of this crate. The previous text described a configuration the manifest
does not produce. Also qualify the azure_core_amqp text: the run-time
error needs the fe2o3_amqp feature, since the no-op backend panics.
The receiver attach test that came in with Azure#4807 calls
RecoverableConnection::new with the previous six arguments, so it stopped
compiling after this branch added the transport argument. Pass the default
transport.
…mple

The open method takes the fully qualified namespace and uses it as the
AMQP host, so the short name in the TransportType example does not
resolve.
The run-time error needs the fe2o3_amqp feature. Without a backend
feature the no-op connection takes over, and its open calls
unimplemented!, so it panics.
`AmqpConnectionOptions` returns to a plain struct with public fields.
The `#[non_exhaustive]` marker and the `with_` methods are gone, so a
struct literal with `..Default::default()` builds it again. The repo
allows the `constructible_struct_adds_field` semver lint for this
reason.

AMQP over WebSockets is now a feature of this crate, not a TLS
selection. `websockets_rustls` and `websockets_native_tls` each turn on
`fe2o3-amqp-ws` and forward one of its TLS backends. `default` selects
`websockets_rustls`, which is rustls with the aws-lc-rs provider, the
same stack `azure_core` selects for HTTP. The direct `rustls`
dependency selects that provider, because `fe2o3-amqp-ws` takes rustls
without default features.

The `fe2o3_amqp` feature no longer pulls in `fe2o3-amqp-ws`, so a build
with that feature alone compiles. The TCP transport keeps
`fe2o3-amqp/native-tls`, which `default` selected before this change:
the rustls backend of `fe2o3-amqp` 0.14 pulls in `ring`, which
`deny.toml` bans (Azure#4189).
`models::TransportType` duplicated `azure_core_amqp::AmqpTransport`,
which this crate already re-exports types from. The enum and its `From`
implementation are gone. `models` re-exports `AmqpTransport` beside
`AmqpMessage` and `AmqpValue`, and the builder method is
`with_transport`, which takes that type.
The WebSocket transport had two features, `websockets_rustls` and
`websockets_native_tls`. Each one turned on the transport and one TLS
stack in a single step, so a consumer could not select the TLS stack
that the rest of the application uses.

Give the transport the base plus modifier shape that `azure_core` uses
for HTTP, where `reqwest` is the base and `reqwest_rustls` adds the
stack. Name the features after the backend crate `fe2o3-amqp-ws`, which
is the rule that `reqwest` and the existing `fe2o3_amqp` both follow.
`fe2o3_amqp_ws` turns on the transport code, and
`fe2o3_amqp_ws_rustls` and `fe2o3_amqp_ws_native_tls` each add one
stack on top of it.

Add `fe2o3_amqp_native_tls` for the TLS stack of the TCP transport.
`default` named `fe2o3-amqp/native-tls` inline before, and `fe2o3-amqp`
is only a dev-dependency of `azure_messaging_eventhubs`, so that crate
could not forward it. Without the named feature, an application that
turned off the default features to select a WebSocket stack also lost
the TLS stack of the TCP transport.

Forward all five features from `azure_messaging_eventhubs`, in the same
way that `azure_core` forwards the `reqwest` TLS features of
`typespec_client_core`. That crate is the one an application depends
on, so the split was not reachable without the forward.

Keep the features additive, which the Cargo reference asks for. A build
can hold both WebSocket stacks; `fe2o3-amqp-ws` then uses native-tls,
and the connection now logs a warning that names the stack in use.
The rebase onto main brought in new tests that build a
`RecoverableConnection` or a `ProducerClient`. Both constructors take
the transport, so each new call site needs the argument. The tests do
not exercise the transport, so they pass the default, which is TCP.
`fe2o3_amqp_ws_native_tls` was a parity feature: it named a second TLS
stack that this crate does not otherwise use. `azure_core` has no such
feature. It offers `reqwest` as the base and `reqwest_rustls` for the
stack it ships, and `samples/list_blobs_native_tls` selects another
stack with a direct `reqwest` dependency and Cargo feature unification.

Follow that pattern. Keep `fe2o3_amqp_ws` as the base and
`fe2o3_amqp_ws_rustls` for the stack this repository ships. Gate the
transport code on the base feature alone, which is what makes
unification work: an application can now name `fe2o3_amqp_ws`, add
`fe2o3-amqp-ws` with `native-tls`, and get the transport on that stack.
The old gate read this crate's own TLS features, so the same manifest
compiled the transport out and returned an error at run time.

One stack must be selected somewhere in the graph, because
`fe2o3-amqp-ws` puts `connect_tls_with_config` behind its own TLS
features. `fe2o3_amqp_ws` on its own therefore does not build. `reqwest`
differs here, since it compiles with no TLS feature at all.

Add `tokio-tungstenite` to the crate dictionary, in both spellings,
beside the other entries.
`fe2o3-amqp` 0.16 builds its rustls backend on aws-lc-rs, where 0.14
built it on `ring`, which `deny.toml` bans. That was the only reason
this crate named native-tls for AMQP framed on TCP.

Update the `fe2o3-amqp` family from 0.14 to 0.16, which needs no source
change, and replace `fe2o3_amqp_native_tls` with `fe2o3_amqp_rustls`.
The `default` feature selects it, so both the TCP and the WebSocket
transport now run on rustls with the aws-lc-rs provider, the stack that
the rest of `sdk/core` uses.

There is no native-tls feature. `fe2o3-amqp` accepts one TLS stack and
reports `TlsConnectorNotFound` at run time when both are on, so a pair
of features that cannot both be on would break `--all-features`. A
consumer that wants another stack turns off the default features and
takes a direct dependency on `fe2o3-amqp` with the stack they want,
which is the pattern `samples/list_blobs_native_tls` shows for
`reqwest`.

The `azure_core_amqp` dependency of `azure_messaging_eventhubs` now
sets `default-features = false`, in the same shape as `azure_core`, so
that turning the default features off can drop the rustls stack.

Closes Azure#4189.
`fe2o3_amqp_ws` is the base feature and names no TLS stack, but the
transport called `WebSocketStream::connect_tls_with_config`, which
`fe2o3-amqp-ws` puts behind its own TLS features. A build that named
`fe2o3_amqp_ws` alone therefore failed with E0599, so the advertised
base feature did not compile.

Call `connect_with_config` instead. It carries no feature gate, and it
reaches the same `tokio_tungstenite` connect path with no connector,
which is what the previous call passed. The behavior is unchanged when a
TLS stack is selected, and a build that selects none now reports
`TlsFeatureNotEnabled` when the connection opens.

This matches `reqwest`, which also compiles with no TLS feature and
fails at run time, and it needs no TLS modifier feature of its own.

Update the feature comment, the `AmqpTransport::WebSocket` doc, and the
changelog, which all said the base feature could not build.
`RecoverableConnection::new` takes a `transport` argument on this branch,
and `ConsumerClientOptions` carries a `transport` field. PR Azure#4933 landed
on main after this branch forked and added a unit test and a builder call
that use the earlier shapes.

Git merges the two changes without a textual conflict, so the branch
built on its own while the pipeline, which builds the merge with main,
failed with E0061, E0063, and E0277. Pass the default transport at both
new call sites.
The `docs.rs` metadata named `fe2o3_amqp`, `fe2o3_amqp_ws`, and
`fe2o3_amqp_ws_rustls`, so docs.rs built the crate without the TLS stack
for AMQP framed on TCP. Add `fe2o3_amqp_rustls`.

The list now holds every feature that `default` selects. It leaves out
`test`, which is an internal helper, and `ffi`, which does not build on
its own (Azure#4978). `azure_core` names its features the same way.
The comment said the receiver and the stream hold references to the
consumer's connection, so the caller must release them before the
consumer closes. That overstates the requirement.

`ConsumerClient::close` takes `&self` on the connection manager and
drains the receiver cache, so it detaches the receiver link on its own.
A receiver that outlives its client then reports the closed client
instead of opening a second connection (Azure#4931). The order of
`receiver.close()` and `consumer.close()` is free.

The one order the caller must keep is the stream before the receiver.
`stream_events` borrows `&self` and `close` takes `self` by value, so
the compiler enforces it. The block above already covers that.
Two entries under Features Added are breaking. Restore the section and
move them.

The `AmqpConnectionOptions::transport` field lands on a struct that is
not `#[non_exhaustive]`, so an existing struct literal that names every
field no longer compiles.

The `default` feature swap from `fe2o3-amqp/native-tls` to
`fe2o3_amqp_rustls` changes the trust anchors. `fe2o3-amqp` builds its
rustls root store from `webpki-roots`, and native-tls read the root
store of the operating system.

The same swap reaches `azure_messaging_eventhubs` through its forwarded
features, so its changelog gets the matching entry.
An explicit port on `custom_endpoint` carries into the `wss://` address,
so a proxy named as `amqps://proxy:5671` dials 5671 and not 443.

The .NET Azure SDK does the same. `AmqpClient` builds its
`ConnectionEndpoint` from the host and the explicit port of
`CustomEndpointAddress`, and `CreateTransportSettingsForWebSockets`
carries that port into the WebSocket URI. A proxy that accepts
WebSockets on its own port stays reachable, so the behavior stands.

Document the rule on `AmqpConnectionOptions::custom_endpoint` and on the
`with_custom_endpoint` builders, and pin the AMQP-port case with a test.
The `rustls` feature of `fe2o3-amqp` 0.16 fills its root store from
`webpki-roots` only, which is a compiled-in copy of the Mozilla root
set. The connection called `open()` with no connector, so AMQP framed
directly on TCP took that default and stopped reading the trust store of
the operating system. A broker behind a private or an enterprise
certificate authority would then fail the handshake, even when the
operating system trusts that authority.

Supply a connector built on `rustls-platform-verifier` instead. The TCP
transport now trusts the same roots as `reqwest/rustls` on the HTTP
side, and it agrees with the WebSocket transport, which already reads
the same store through native roots.

Use `rustls_connector`, because it needs the `rustls` feature only,
where `tls_connector` also needs `native-tls` to be off. The
connector-typed builder hands `alt_tls_estab` to the same transport call
as the default path, so `alt_tls_establishment(true)` keeps its
behavior. Add a test that builds the connector, which catches a missing
crypto provider at test time instead of at connect time.
@j7nw4r
Johnathan W (j7nw4r) force-pushed the worktree-eventhubs-amqp-websockets branch from 0b02452 to ff03820 Compare August 7, 2026 13:27

@SwayGom Josue Gomez (SwayGom) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving — the design is right and the implementation is careful. Keeping the AMQP hostname separate from the socket target on the WebSocket path is the detail that matters most here, and it is correct. Splitting the base transport feature from the TLS stack feature (fe2o3_amqp_ws / fe2o3_amqp_ws_rustls) also matches the existing reqwest / reqwest_rustls shape in this repo.

Two things I would like tracked, neither of which I think should hold the PR:

1. The two transports do not use the same TLS trust mechanism.

The TCP path builds its connector from rustls-platform-verifier, which delegates chain verification to the operating system. The WebSocket path goes through fe2o3-amqp-ws/rustls-tls-native-roots, which only seeds a rustls root store and then verifies in rustls. Those are not equivalent: they can differ on revocation, cross-signed intermediates, and enterprise policy. The PR description says the WebSocket path "reads the same store through native roots", which reads as stronger than what the code actually gives us (the CHANGELOG is more careful).

The practical risk is a customer who switches to WebSockets to get around a blocked 5671 and then hits a TLS failure that looks unrelated to the transport change. Worth an issue to either align the two paths or document the difference explicitly.

2. The AmqpConnectionOptions breaking change is still a breaking change.

The CHANGELOG is honest about this: the struct is not #[non_exhaustive], so an exhaustive struct literal no longer compiles, and the constructible_struct_adds_field semver lint is allowlisted rather than satisfied. That trade also applies to every future field on this struct, not just transport. If the intent is for field additions to be non-breaking going forward, #[non_exhaustive] is the way to get that from the type system instead of from documentation.

Smaller notes, take or leave:

  • Err(...)?; unreachable!() in the #[cfg(not(feature = "fe2o3_amqp_ws"))] arm would read better as a plain return Err(...).
  • platform_verifier_connector_builds asserts is_ok(), but the failure mode its comment describes is a panic, so the test aborts rather than failing on the case it documents.
  • The deliberate runtime error for AmqpTransport::WebSocket without the fe2o3_amqp_ws feature is the right API call, but there is no test covering it.

Note that CI is currently red on all six Build Test jobs plus Build Analyze, and there is an open changes-requested review, so this still needs both resolved before it can merge.

@j7nw4r

Copy link
Copy Markdown
Member Author

Closing this for #5031, which carries the same commits on a branch in this repo.

#4928 moved cargo onto the Azure Artifacts feed, and that feed wants a token even to read. Fork PRs do not get one, so the build authenticates as the anonymous user and the feed answers 401 before cargo resolves anything. That fails all six jobs before anything compiles. Every fork build in this repo has hit it since August 6. Moving the branch here is the only way I get a pipeline that runs.

Same code, rebased on current main. Heath Stewart (@heaths), the open threads did not come across, so I will carry them to #5031 and answer them there. Larry Osterman (@LarryOsterman) and Josue Gomez (@SwayGom), you will need to re-approve on the new one.

auto-merge was automatically disabled August 8, 2026 18:11

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure.Core The azure_core crate Event Hubs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot use aws-lc-rs for AMQP

6 participants