Azure / Azure/azure-sdk-for-rust
Support !Send HTTP clients for single-threaded async runtimes
- Dominant language
- Rust
- Stars
- 884
- Forks
- 365
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 112
Description
### Feature Summary
The `HttpClient` trait (and the entire `Policy` / `Pipeline` stack) requires `Send + Sync`, which prevents integration with async runtimes that use `!Send` types internally — notably [ntex](https://github.com/ntex-rs/ntex), which uses `Rc`/`RefCell` for performance in its single-threaded model.
### Feature Description
## Motivation
The Azure SDK for Rust currently hardcodes [reqwest](https://github.com/seanmonstar/reqwest) as the HTTP transport, which depends on [tokio](https://github.com/tokio-rs/tokio). This creates problems for applications using non-tokio async runtimes:
- **[ntex](https://github.com/ntex-rs/ntex)** — a high-performance async framework that supports multiple I/O backends (tokio, compio/IOCP, io_uring, epoll). Its HTTP client (`ntex::client::Client`) is `!Send` because it uses `Rc`/`RefCell` to avoid atomic overhead in single-threaded contexts.
- **[compio](https://github.com/compio-rs/compio)** — a completion-based async runtime for Windows (IOCP) and Linux (io_uring). Types are typically `!Send`.
- **[glommio](https://github.com/DataDog/glommio)** — a thread-per-core runtime where `!Send` is by design.
When using these runtimes, the Azure SDK panics at runtime because reqwest tries to use `tokio::net::TcpStream` and there is no tokio reactor running:
```
thread panicked at tokio/src/net/tcp/stream.rs:164:18:
there is no reactor running, must be called from the context of a Tokio 1.x runtime
```
## Current state
The `Send + Sync` requirement is on:
1. **`HttpClient` trait** (`typespec_client_core/src/http/clients/mod.rs:36`):
```rust
pub trait HttpClient: Send + Sync + std::fmt::Debug { ... }
```
2. **`Policy` trait** (`typespec_client_core/src/http/policies/mod.rs:29`):
```rust
pub trait Policy: Send + Sync + std::fmt::Debug { ... }
```
3. **`Pipeline`** stores `Vec>`, propagating the `Send` requirement.
4. **`Transport`** stores `Arc`, propagating the `Send` requirement.
The SDK already relaxes `Send` for **wasm32** targets:
```rust
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait HttpClient: Send + Sync + std::fmt::Debug { ... }
```
This precedent shows the architecture can work without `Send`.
## Proposal
Extend the wasm32 pattern to support `!Send` HTTP clients on native targets. Some possible approaches:
### Option A: Feature flag (simplest)
Add a cargo feature like `single-threaded` or `no-send` that relaxes the `Send + Sync` bounds:
```rust
#[cfg(any(target_arch = "wasm32", feature = "single-threaded"))]
pub trait HttpClient: std::fmt::Debug { ... }
#[cfg(not(any(target_arch = "wasm32", feature = "single-threaded")))]
pub trait HttpClient: Send + Sync + std::fmt::Debug { ... }
```
This would need to propagate through `Policy`, `Pipeline`, and `Transport` similarly.
### Option B: Generic over Send bound
Use a type parameter or associated type to make the `Send` bound configurable:
```rust
pub trait HttpClient: std::fmt::Debug { ... }
```
### Option C: Separate trait
Provide a `LocalHttpClient` trait without `Send + Sync` bounds, with a `LocalPipeline` that accepts it.
## Impact
This would enable the Azure SDK to work natively with:
- ntex (compio, io_uring, epoll backends)
- compio
- glommio
- monoio
- Any other single-threaded async runtime
## Workaround (current)
We bridge the `Send` gap using a request/response serialization pattern:
1. `NtexHttpClient` struct has no fields → automatically `Send + Sync`
2. In `execute_request`, request data (method, URL, headers, body) is serialized into plain `Send`-safe types (`String`, `Vec`)
3. `ntex_util::spawn` runs the actual HTTP call on the current thread's local task queue using a `thread_local!` `ntex::client::Client` (lazily initialized)
4. Response data (status, headers, body bytes) flows back via `async-channel` as `Send`-safe types
5. The caller converts the response back to `AsyncRawResponse`
This adds unnecessary serialization overhead and complexity, but avoids spawning extra threads and keeps all I/O on the current ntex thread.
### Use Case
applications using non-tokio async runtimes such as ntex, compio, glommio
### Alternatives
The work around is described above, but it's a bit hacky and we'd prefer to have `!Send` support on the trait
### Additional Context
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.