zenwave 0.1.0

Ergonomic HTTP client framework.
Documentation

Zenwave

crates.io Documentation License Coverage

Zenwave is an ergonomic, full-featured HTTP client framework for Rust. It exposes a modern, middleware-friendly API that works on both native targets (Tokio + Hyper on Linux/Windows, Apple's URLSession on iOS/tvOS/watchOS/macOS) and browser/Cloudflare Workers targets through the Fetch API.

Why Zenwave?

  • Ergonomic requests – convenience helpers (get, post, …) and a fluent RequestBuilder.
  • Opt-in middleware – add redirect following, cookie storage, OAuth2 refresh, or redirects only when you need it.
  • Streaming bodies – handle large uploads/downloads or upgrade to SSE without buffering.
  • HTTP caching – drop-in middleware honors Cache-Control, Expires, ETag, and Last-Modified to avoid redundant network hops.
  • Native timers – enforce per-request deadlines with high-precision timers on every supported platform via a simple .timeout(...) helper.
  • Proxy aware – honor HTTP(S)_PROXY/NO_PROXY or define custom SOCKS/HTTP proxies when using the Hyper or curl backends.
  • WebSocket ready – one API that works natively and in WASM.
  • Pluggable backends – Hyper on general native targets, URLSession on Apple platforms, libcurl when you want small binaries, and Fetch on wasm, all behind the same zenwave::client() interface.

Quick Start

use zenwave::{get, ResponseExt};

#[async_std::main]
async fn main() -> zenwave::Result<()> {
    let response = get("https://example.com/").await?;
    let text = response.into_string().await?;
    println!("{text}");
    Ok(())
}

The ResponseExt trait provides the into_string, into_json, into_bytes, and into_sse helpers you will see throughout the API.

Examples

Run the shipped samples with cargo run --example <name>:

  • basic_get – one-off GET request parsed into a typed response.
  • custom_client – compose middleware, send JSON, and read a typed response body.
  • websocket_echo – connect to a public echo server using the cross-platform WebSocket client.

Feel free to copy these examples as starting points for your own projects.

Building richer clients

use std::time::Duration;

use serde::{Deserialize, Serialize};
use zenwave::{self, Cache, Client, OAuth2ClientCredentials};

#[derive(Serialize)]
struct MessageRequest<'a> {
    message: &'a str,
}

#[derive(Deserialize)]
struct EchoResponse {
    json: MessageResponse,
}

#[derive(Deserialize)]
struct MessageResponse {
    message: String,
}

#[async_std::main]
async fn main() -> zenwave::Result<()> {
    let token = std::env::var("ZENWAVE_TOKEN").unwrap_or_else(|_| "demo-token".into());

    // Compose only the middleware you need.
    let client = zenwave::client()
        .timeout(Duration::from_secs(2))
        .enable_cache()
        .with(OAuth2ClientCredentials::new(
            "https://auth.example.com/token",
            "client-id",
            "client-secret",
        ))
        .follow_redirect()
        .enable_cookie()
        .bearer_auth(token);

    let echo: EchoResponse = client
        .post("https://httpbin.org/post")
        .header("x-request-id", "demo-request")
        .json_body(&MessageRequest { message: "hello" })?
        .json()
        .await?;

    println!("{}", echo.json.message);
    Ok(())
}

You can also call .basic_auth or .with(custom_middleware) to plug in your own behavior. Every request builder supports .header, .bearer_auth, .basic_auth, .json_body, .bytes_body, and body readers (.json(), .string(), .bytes(), .form(), .sse()).

Timeouts are middleware too. Calling .timeout(Duration::from_secs(2)) wraps the client in a native-executor-backed timer so every subsequent request automatically fails with a 504 Gateway Timeout when the deadline is exceeded.

Proxy configuration (native Hyper / curl backends)

Zenwave can route requests through HTTP or SOCKS proxies by reading the standard HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY variables or by constructing a matcher manually. Both the Hyper and libcurl-native backends honor the same configuration:

use zenwave::{self, Proxy};

fn main() {
    // Inherit proxy settings from the environment (`*_PROXY` / `NO_PROXY`).
    let proxy = Proxy::from_env();
    let client = zenwave::client_with_proxy(proxy);

    // Or build one manually. Supports http, socks4, socks4a, socks5, and socks5h schemes.
    let custom = Proxy::builder()
        .http("http://corp-proxy:8080")
        .no_proxy("internal.example.com")
        .build();
    let mut custom_client = zenwave::client_with_proxy(custom);

    // Clients returned by `zenwave::client()` can also be swapped afterwards:
    let mut swapped = zenwave::client().proxy(Proxy::from_system());
}

Only the Hyper and curl backends currently honor proxies. HTTP CONNECT proxies (http:// / https://) and SOCKS4/5 proxies (socks4[a], socks5[h]) are supported. The Apple (apple-backend) and Web (wasm32) backends do not expose proxy APIs, so helper functions such as client_with_proxy or .proxy(...) are not compiled when those backends are selected as the default.

Large downloads with resume

Native targets get an ergonomic helper for writing large responses to disk without buffering into memory. Any request builder can call download_to_path to stream the body into a file. When the file already exists Zenwave automatically issues a Range request and appends only the missing bytes, so interrupted transfers can resume without starting from scratch.

use zenwave::Client;

# async fn example() -> zenwave::Result<()> {
let client = zenwave::client();
let report = client
    .get("https://example.com/big.iso")
    .download_to_path("big.iso")
    .await?;

println!(
    "Resumed from {} bytes and wrote {} bytes ({} total)",
    report.resumed_from,
    report.bytes_written,
    report.total_bytes()
);
# Ok(())
# }

If you need to opt out of resume logic you can call download_to_path_with and pass DownloadOptions { resume_existing: false }. Both methods return a DownloadReport so you can log how much data was appended and what now resides on disk. This helper is currently available on non-wasm targets where direct filesystem access exists.

Streaming uploads

Use file_body to upload large files directly from disk without buffering, reader_body to wrap any AsyncRead, or stream_body to hook up custom chunk producers. Each helper integrates with Tokio so uploads backpressure naturally with the network stack.

# async fn example() -> zenwave::Result<()> {
use zenwave::client;
use async_fs::File;

let mut client = client();
let response = client
    .post("https://example.com/upload")
    .file_body("video.mp4")
    .await?
    .await?;
assert!(response.status().is_success());

let mut stream_client = client();
let file = File::open("log.txt").await?;
let response = stream_client
    .post("https://example.com/logs")
    .reader_body(file, None)
    .await?;
assert!(response.status().is_success());
# Ok(())
# }

HTTP cache middleware

Call .enable_cache() to enable RFC-compliant client-side caching. The middleware caches successful GET responses when permitted by Cache-Control/Expires, automatically injects validators for stale entries (If-None-Match, If-Modified-Since), and serves 304 Not Modified responses straight from memory. Requests with Authorization headers are skipped unless the response explicitly declares itself public. Because it is implemented as middleware you can keep it for native builds only or combine it with other layers as needed.

Persistent cookie store

Call .enable_persistent_cookie() to transparently load and save cookies between runs on native targets. Zenwave automatically picks a cache file under your platform's local data directory using the name zenwave_cookie_store_<crate_name>.json, so crates only share cookies with themselves by default. You can also fully control the path via CookieStore::persistent_with_path if you want to sync cookies across binaries.

OAuth2 client credentials

Use OAuth2ClientCredentials::new(token_url, client_id, client_secret) to automatically obtain and refresh bearer tokens. The middleware performs the client credentials flow against the configured token endpoint, caches responses until they near expiration, and injects the Authorization header for every outgoing request. Call .with_scope("scope1 scope2") or .with_audience("api") if your provider requires additional parameters.

Web & Cloudflare Workers

Zenwave targets both wasm32 and native platforms. On wasm it relies on web_sys::Request/Fetch, so it works in browsers and Cloudflare Workers without extra glue code. The API is identical, so sharing code between targets is straightforward.

Apple platforms

By default Apple targets (iOS, iPadOS, tvOS, watchOS, macOS) also use the Hyper backend. There is an experimental apple-backend feature that swaps Hyper out for URLSession, which satisfies watchOS/App Store restrictions but currently auto-follows redirects and auto-manages cookies. The two middleware tests that asserted “no redirect / no automatic cookies” are skipped whenever the apple-backend feature is enabled. Until the URLSession backend is stabilized, we recommend keeping the default Hyper backend on Apple. If you still want to opt in:

zenwave = { version = "0.1.0", features = ["apple-backend"] }

Curl backend

Many Linux distributions (and some embedded platforms) ship a system libcurl. Zenwave can reuse it to avoid bundling Hyper/OpenSSL and shrink your binary. Disable the default features and enable the curl backend:

[dependencies]
zenwave = { version = "0.1.0", default-features = false, features = ["curl-backend"] }

You still get the same middleware API; the only difference is which backend transports the bytes.

WebSocket support

The zenwave::websocket module offers a cross-platform WebSocket client that hides the details of async-tungstenite or web_sys::WebSocket. Connecting to an endpoint looks like:

use zenwave::websocket::{self, WebSocketMessage};

#[async_std::main]
async fn main() -> zenwave::Result<()> {
    let mut socket = websocket::connect("wss://echo.websocket.events").await?;
    socket.send_text("hello").await?;

    if let Some(WebSocketMessage::Text(text)) = socket.recv().await? {
        println!("Received: {text}");
    }

    socket.close().await
}

You can also split a connection to drive sending and receiving from different tasks:

let socket = websocket::connect("wss://echo.websocket.events").await?;
let (sender, receiver) = socket.split();

// `send` serializes to JSON by default; use `send_text` for raw text frames.
sender.send(&MyPayload { message: "hello" }).await?;
if let Some(reply) = receiver.recv().await? {
    println!("Got reply: {:?}", reply);
}

Installation

Add Zenwave to your Cargo.toml. The default configuration uses the Hyper backend with rustls TLS:

[dependencies]
zenwave = { version = "0.1.0" }

For browser/Workers builds, no special configuration is needed - Zenwave automatically uses the built-in web backend (Fetch API) on wasm32 targets:

# For wasm32 targets, default features are ignored and the web backend is used automatically
zenwave = { version = "0.1.0" }

Feature flags

Backend Selection (native platforms only)

On wasm32 targets, the built-in web backend is always used automatically. No backend selection is needed or available.

On native platforms, you can choose from:

  • hyper-backend (default) – Hyper with async-io/async-net. The recommended choice for most use cases.
  • curl-backend – libcurl-based backend with built-in proxy support. Good for platforms with system libcurl.
  • apple-backend – experimental URLSession backend for Apple platforms (macOS/iOS).

TLS Selection (hyper-backend only)

  • rustls (default) – pure-Rust TLS implementation. Cross-platform and secure.
  • native-tls – uses the platform's native TLS (OpenSSL on Linux, Secure Transport on macOS, SChannel on Windows).

Only one TLS feature can be enabled at a time. These features only apply to hyper-backend; other backends have their own TLS implementations.

Other Features

  • proxy – enables proxy support (automatically enabled with curl-backend).

Example configurations

# Use curl backend instead of hyper
zenwave = { version = "0.1.0", default-features = false, features = ["curl-backend"] }

# Use hyper with native-tls instead of rustls
zenwave = { version = "0.1.0", default-features = false, features = ["hyper-backend", "native-tls"] }

# Use Apple's native URLSession on macOS/iOS
zenwave = { version = "0.1.0", default-features = false, features = ["apple-backend"] }

License

MIT License