# Zenwave
[](https://crates.io/crates/zenwave)
[](https://docs.rs/zenwave)
[](LICENSE)
[](https://app.codecov.io/gh/zen-rs/zenwave)
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
```rust
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
```rust
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:
```rust
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.
```rust
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.
```rust
# 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:
```toml
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:
```toml
[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:
```rust
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:
```rust
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:
```toml
[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:
```toml
# 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
```toml
# 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