aioduct 0.1.10

Async-native HTTP client built directly on hyper 1.x — no hyper-util, no legacy
Documentation

aioduct

Crates.io docs.rs CI License: MIT OR Apache-2.0 MSRV: 1.88

Async-native Rust HTTP client built directly on hyper 1.x — no hyper-util, no legacy APIs.

Documentation | API Reference | Crates.io

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
  • Multipartmultipart/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

[dependencies]
aioduct = { version = "0.1", features = ["tokio"] }
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:

aioduct = { version = "0.1", features = ["tokio", "rustls", "rustls-ring"] }

To use rustls with AWS-LC instead of ring, select the AWS-LC provider:

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:

aioduct = { version = "0.1", features = ["tokio", "rustls-native-roots", "rustls-aws-lc-rs"] }
let client = Client::<TokioRuntime>::with_rustls();
let resp = client.get("https://httpbin.org/get")?.send().await?;

Feature Flags

Feature Description Stability
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

// 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

let resp = client.post("https://example.com/login")?
    .form(&[("username", "admin"), ("password", "secret")])
    .send()
    .await?;

Authentication

// 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

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

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

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

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

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)

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.

# 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.

# 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:

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

reqwest aioduct
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

at your option.