# aioduct
[](https://crates.io/crates/aioduct)
[](https://docs.rs/aioduct)
[](https://github.com/adamcavendish/aioduct/actions/workflows/ci.yml)
[](LICENSE-MIT)
[](https://blog.rust-lang.org/2025/06/26/Rust-1.88.0.html)
Async-native Rust HTTP client built directly on **hyper 1.x** — no hyper-util, no legacy APIs.
## Why aioduct?
- **reqwest** depends on hyper-util's `legacy::Client`, wrapping hyper 0.x-style patterns over hyper 1.x with years of backwards-compatibility baggage.
- **hyper-util** labels its own client as "legacy" — the hyper team acknowledges it's not the long-term answer.
- **hyper 1.x** provides clean connection-level primitives, but no production client uses them directly.
aioduct uses hyper 1.x **the way it was intended** — as a protocol engine you drive yourself, with your own connection pool, TLS, and runtime integration.
## Features
- **No hyper-util** — custom IO adapters and executor directly against `hyper::rt` traits
- **Multi-runtime** — tokio, smol, and compio (io_uring) via feature flags; WASM/browser support
- **rustls TLS** — async handshake with ALPN-based HTTP/1.1 and HTTP/2 negotiation
- **Connection pooling** — keyed by (scheme, authority) with idle timeout and per-host limits
- **Redirect following** — RFC-compliant handling of 301/302/303/307/308 with sensitive header stripping and content header removal
- **Cookie jar** — automatic cookie storage, domain/path/subdomain matching, Max-Age and Expires expiration, Secure flag enforcement, SameSite (Strict/Lax/None), cookie prefixes (__Host-, __Secure-)
- **Timeouts** — per-request, client-level, connect, and read timeouts
- **Retry** — configurable exponential backoff with retry budgets, Retry-After header support, 429 Too Many Requests retry
- **Decompression** — automatic gzip, brotli, zstd, deflate response decompression
- **Proxy** — HTTP CONNECT tunneling, SOCKS4/SOCKS4a, SOCKS5, system proxy detection (HTTP_PROXY/HTTPS_PROXY/NO_PROXY)
- **Middleware** — pluggable request/response interceptors via trait or closure
- **Rate limiting** — token-bucket rate limiter for outgoing requests
- **Caching** — in-memory HTTP cache with immutable responses, stale-while-revalidate, stale-if-error (fallback on 5xx/connection failure); pluggable `CacheStore` trait for custom backends
- **HSTS** — automatic HTTP-to-HTTPS upgrade for Strict-Transport-Security domains
- **SSE** — Server-Sent Events stream parsing for LLM APIs
- **Multipart** — `multipart/form-data` uploads with text fields and file parts
- **Streaming** — chunked downloads and streaming uploads without buffering
- **Chunk download** — parallel HTTP Range requests for large files
- **HTTP upgrade** — WebSocket and other protocol upgrades via HTTP/1.1 101 and HTTP/2 extended CONNECT (RFC 8441)
- **Request forwarding** — proxy/gateway builder via `Client::forward(req)` that strips hop-by-hop headers, rewrites URIs, streams bodies, auto-detects WebSocket upgrades, supports H2 extended CONNECT tunneling, per-forward h2c for gRPC upstreams, and adaptive h2c/h1 fallback with per-authority capability caching
- **Blocking client** — synchronous wrapper for non-async contexts (requires tokio)
- **Custom DNS** — pluggable resolver via the `Resolve` trait; hickory-dns integration; DNS-over-HTTPS (`doh` feature) and DNS-over-TLS (`dot` feature)
- **HTTP/2 tuning** — configurable window sizes, frame size, adaptive window, keepalive PINGs
- **Connection coalescing** — reuses h2/h3 connections whose TLS certificate SANs cover the target domain (RFC 7540 §9.1.1), matching browser behavior
- **HTTP/3 0-RTT** — opt-in early data for repeat connections to known servers, with automatic fallback on rejection
- **TCP keepalive** — configurable keepalive interval for long-lived connections
- **TCP Fast Open** — reduced connection latency on Linux via TCP_FASTOPEN_CONNECT
- **Local address binding** — bind outgoing connections to a specific local IP
- **JSON** — optional `json` feature for request/response serialization
- **Problem Details** — RFC 9457 `application/problem+json` response parsing (requires `json` feature)
- **Happy Eyeballs** — RFC 6555 connection racing, interleaves IPv6/IPv4 with 250ms stagger
- **Digest auth** — automatic HTTP Digest authentication with 401 retry (RFC 7616, MD5)
- **Bandwidth limiter** — token-bucket byte-rate throttle for download speed limiting
- **Netrc** — `.netrc` file parser and middleware for automatic credential injection
- **Auth helpers** — bearer token, basic auth
- **Form data** — URL-encoded form bodies
- **Query parameters** — with percent-encoding
- **Default headers** — automatic User-Agent, configurable defaults
- **Observability** — optional tracing spans and OpenTelemetry middleware
- **Tower integration** — use aioduct as a tower `Service`
- **Link headers** — RFC 8288 Link header parsing for pagination and discovery
- **Forwarded header** — RFC 7239 Forwarded header builder and parser
- **Request timings** — per-request DNS, TCP connect, TLS handshake, transfer (TTFB), and total durations via `Response::timings()`
## Quick Start
```toml
[dependencies]
aioduct = { version = "0.1", features = ["tokio"] }
```
```rust
use aioduct::{Client, StatusCode};
use aioduct::runtime::TokioRuntime;
#[tokio::main]
async fn main() -> Result<(), aioduct::Error> {
let client = Client::<TokioRuntime>::new();
let resp = client.get("http://httpbin.org/get")?
.send()
.await?;
assert_eq!(resp.status(), StatusCode::OK);
println!("{}", resp.text().await?);
Ok(())
}
```
## HTTPS
Enable the `rustls` TLS backend plus exactly one rustls crypto provider:
```toml
aioduct = { version = "0.1", features = ["tokio", "rustls", "rustls-ring"] }
```
To use rustls with AWS-LC instead of ring, select the AWS-LC provider:
```toml
aioduct = { version = "0.1", features = ["tokio", "rustls", "rustls-aws-lc-rs"] }
```
To use the OS certificate store, add `rustls-native-roots` alongside either TLS provider:
```toml
aioduct = { version = "0.1", features = ["tokio", "rustls-native-roots", "rustls-aws-lc-rs"] }
```
```rust
let client = Client::<TokioRuntime>::with_rustls();
let resp = client.get("https://httpbin.org/get")?.send().await?;
```
## Feature Flags
| `tokio` | Tokio async runtime | Stable |
| `smol` | Smol async runtime | Stable |
| `compio` | Compio runtime (io_uring / IOCP) | Experimental |
| `wasm` | Browser/WASM runtime via web-sys | Experimental |
| `rustls` | TLS via rustls; requires exactly one rustls provider | Stable |
| `rustls-ring` | ring crypto provider for rustls | Stable |
| `rustls-aws-lc-rs` | AWS-LC crypto provider for rustls | Stable |
| `rustls-native-roots` | Use OS certificate store with either rustls provider | Stable |
| `json` | JSON request/response with serde | Stable |
| `charset` | Charset decoding via encoding_rs | Stable |
| `gzip` | Gzip response decompression | Stable |
| `deflate` | Deflate response decompression | Stable |
| `brotli` | Brotli response decompression | Stable |
| `zstd` | Zstd response decompression | Stable |
| `blocking`| Synchronous blocking client (requires tokio) | Stable |
| `hickory-dns` | DNS via hickory-resolver (requires tokio) | Stable |
| `doh` | DNS-over-HTTPS (implies `hickory-dns`) | Stable |
| `dot` | DNS-over-TLS (implies `hickory-dns`) | Stable |
| `tower` | Tower `Service` and `Layer` integration | Stable |
| `tracing` | Tracing spans for requests | Stable |
| `otel` | OpenTelemetry middleware | Stable |
| `http3` | HTTP/3 transport via h3 + h3-quinn; currently requires `rustls` plus one rustls provider | Experimental |
At least one runtime feature must be enabled or compilation will fail. When `rustls` is enabled, choose exactly one of `rustls-ring` or `rustls-aws-lc-rs`. The `native-tls` backend name is reserved for possible future OpenSSL/native TLS support and is not implemented today.
## Examples
### JSON
```rust
// Requires features = ["tokio", "json"]
let resp = client.post("https://api.example.com/users")?
.json(&serde_json::json!({"name": "Alice"}))?
.send()
.await?;
let user: User = resp.json().await?;
```
### Form Data
```rust
let resp = client.post("https://example.com/login")?
.form(&[("username", "admin"), ("password", "secret")])
.send()
.await?;
```
### Authentication
```rust
// Bearer token
let resp = client.get("https://api.example.com/me")?
.bearer_auth("my-token")
.send()
.await?;
// Basic auth
let resp = client.get("https://example.com/protected")?
.basic_auth("user", Some("pass"))
.send()
.await?;
```
### Query Parameters
```rust
let resp = client.get("https://example.com/search")?
.query(&[("q", "hello world"), ("page", "1")])
.send()
.await?;
// GET /search?q=hello%20world&page=1
```
### Client Configuration
```rust
use std::time::Duration;
let client = Client::<TokioRuntime>::builder()
.timeout(Duration::from_secs(30))
.max_redirects(5)
.pool_idle_timeout(Duration::from_secs(90))
.pool_max_idle_per_host(10)
.tcp_keepalive(Duration::from_secs(60))
.local_address("192.168.1.100".parse().unwrap())
.build();
```
### SOCKS5 Proxy
```rust
use aioduct::ProxyConfig;
let client = Client::<TokioRuntime>::builder()
.proxy(ProxyConfig::socks5("socks5://proxy.example.com:1080").unwrap())
.build();
// With authentication
let client = Client::<TokioRuntime>::builder()
.proxy(
ProxyConfig::socks5("socks5://proxy.example.com:1080")
.unwrap()
.basic_auth("user", "pass"),
)
.build();
```
### HTTP/2 Tuning
```rust
use aioduct::Http2Config;
let client = Client::<TokioRuntime>::builder()
.tls(aioduct::tls::RustlsConnector::with_webpki_roots())
.http2(
Http2Config::new()
.initial_stream_window_size(2 * 1024 * 1024)
.adaptive_window(true)
.keep_alive_interval(Duration::from_secs(20))
.keep_alive_while_idle(true),
)
.build();
```
### Smol Runtime
```rust
use aioduct::Client;
use aioduct::runtime::SmolRuntime;
smol::block_on(async {
let client = Client::<SmolRuntime>::new();
let resp = client.get("http://httpbin.org/get")?
.send()
.await?;
println!("{}", resp.text().await?);
Ok::<_, aioduct::Error>(())
});
```
### Request Forwarding (Reverse Proxy)
```rust
use aioduct::{Client, Protocol};
use aioduct::runtime::TokioRuntime;
use bytes::Bytes;
use http_body_util::Full;
# async fn example() -> Result<(), aioduct::Error> {
let client = Client::<TokioRuntime>::new();
// Forward a request to an upstream, stripping a path prefix
let incoming_req = http::Request::builder()
.method("GET")
.uri("/api/users?page=2")
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client
.forward(incoming_req)
.upstream("http://backend:8080".parse::<http::Uri>().unwrap())
.strip_prefix("/api")
.header(
http::header::HeaderName::from_static("x-forwarded-for"),
http::header::HeaderValue::from_static("10.0.0.1"),
)
.send()
.await?;
// WebSocket upgrade forwarding (auto-detected from headers)
let ws_req = http::Request::builder()
.method("GET")
.uri("/ws/chat")
.header("connection", "Upgrade")
.header("upgrade", "websocket")
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client
.forward(ws_req)
.upstream("http://ws-backend:9000".parse::<http::Uri>().unwrap())
.send()
.await?;
let upstream_io = resp.upgrade().await?;
// Splice with downstream: tokio::io::copy_bidirectional(...)
# Ok(())
# }
```
## CLI Tools
The workspace includes two CLI tools built on aioduct:
### aioduct-aria
An aria2-inspired parallel download tool. Splits large files into segments and downloads them concurrently using HTTP Range requests.
```sh
# Download with 8 segments
aioduct-aria -s 8 https://example.com/large-file.tar.gz
# Resume an interrupted download
aioduct-aria -c https://example.com/large-file.tar.gz
```
### aioduct-curl
A curl-inspired HTTP tool with familiar flags.
```sh
# GET request
aioduct-curl https://httpbin.org/get
# POST with JSON body
aioduct-curl -X POST -d '{"key":"val"}' -H 'Content-Type: application/json' https://httpbin.org/post
# Follow redirects, basic auth, save to file
aioduct-curl -L -u user:pass -o output.html https://example.com
```
Both tools are workspace members (`publish = false`) and serve as real-world integration examples.
## Architecture
```
Client<R: Runtime>
├── RequestBuilder ← fluent API (headers, body, auth, query, timeout)
├── ConnectionPool<R> ← keyed by (scheme, authority), idle eviction
├── TLS (rustls) ← async handshake, ALPN → h1/h2
└── Runtime trait ← TcpStream, Sleep, spawn, resolve
├── TokioRuntime
├── SmolRuntime
└── CompioRuntime
```
The `Runtime` trait abstracts over async runtimes:
```rust
pub trait Runtime: Send + Sync + 'static {
type TcpStream: hyper::rt::Read + hyper::rt::Write + Send + Unpin + 'static;
type Sleep: Future<Output = ()> + Send;
async fn connect(addr: SocketAddr) -> io::Result<Self::TcpStream>;
async fn resolve(host: &str, port: u16) -> io::Result<SocketAddr>;
fn sleep(duration: Duration) -> Self::Sleep;
fn spawn<F: Future<Output = ()> + Send + 'static>(future: F);
}
```
## Comparison
| hyper | 1.x via hyper-util legacy | 1.x direct |
| hyper-util | Required | Not used |
| Runtime | tokio only | tokio / smol / compio / wasm |
| TLS | rustls or native-tls | rustls (`native-tls` reserved for future support) |
| HTTP/3 | Experimental | Experimental |
| io_uring | No | Via compio |
| Connection pool | hyper-util legacy | Custom h1/h2/h3 |
| Cookie jar | Yes | Yes |
| SSE streaming | No (manual) | Built-in |
| Rate limiting | No | Built-in |
| HTTP caching | No | Built-in |
| HSTS | No | Built-in |
| Link headers | No | Built-in |
| Problem Details | No | Built-in |
| Middleware | Via tower | Built-in + tower |
| Happy Eyeballs | No | RFC 6555 |
| Digest auth | No | Built-in |
| Bandwidth limiter | No | Built-in |
| Netrc | No | Built-in |
| Request timings | No | Built-in |
| Connection coalescing | No | Built-in (RFC 7540) |
| DNS-over-HTTPS/TLS | No | Built-in |
| HTTP/3 0-RTT | No | Built-in |
| Request forwarding | No | Built-in |
## MSRV
The minimum supported Rust version is **1.88.0** (edition 2024).
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT License ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option.