HTTP Clients (reqwest)
Call external HTTP APIs from Rust with reqwest - async requests, JSON, timeouts, and rustls TLS.
Recipe
Quick-reference recipe card - copy-paste ready.
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let user: User = client
.get("https://api.example.com/users/1")
.bearer_auth(token)
.send()
.await?
.error_for_status()?
.json()
.await?;When to reach for this: Any Rust service integrating with REST webhooks, payment APIs, or internal microservices.
Working Example
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct CreateItem { name: String }
#[derive(Deserialize)]
struct Item { id: u64, name: String }
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.user_agent("my-rust-service/1.0")
.build()?;
let created: Item = client
.post("https://httpbin.org/post")
.json(&CreateItem { name: "widget".into() })
.send()
.await?
.error_for_status()?
.json()
.await?;
let status = client.get("https://httpbin.org/status/404").send().await?;
assert_eq!(status.status(), StatusCode::NOT_FOUND);
Ok(())
}What this demonstrates:
- Shared
Clientreused across requests (connection pooling). .json()for serde request and response bodies.error_for_status()turns 4xx/5xx into errors.- Global timeout on the builder.
Deep Dive
How It Works
- reqwest uses hyper under the hood with Tokio.
- One
Clientper process maintains connection pool to hosts. - Enable
rustls-tlsfeature for TLS without OpenSSL.
Client Checklist
| Setting | Purpose |
|---|---|
timeout | End-to-end request limit |
connect_timeout | TCP/TLS handshake cap |
pool_max_idle_per_host | Keep-alive tuning |
user_agent | Identify your service in logs |
Rust Notes
// Reuse client in Axum State
#[derive(Clone)]
struct HttpClients { outbound: reqwest::Client }Gotchas
- New Client per request - Destroys connection reuse. Fix: Build once at startup.
- No timeout - Hung upstream blocks worker forever. Fix: Always set timeouts.
- Ignoring error bodies -
error_for_statusdrops useful JSON errors. Fix: Read body on failure for logging. - TLS verify disabled -
danger_accept_invalid_certsin prod. Fix: Proper CA bundle with rustls. - Huge response bodies -
json().awaitloads all into memory. Fix: Stream withbytes_stream.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
hyper client directly | Full control | Standard REST calls |
ureq | Blocking CLI tools | Async Tokio services |
awc | Actix ecosystem | Axum/Tokio stack |
FAQs
reqwest blocking?
reqwest::blocking for sync contexts only.
Custom headers?
.header("X-Request-Id", id) per request or default headers on builder.
Multipart?
multipart::Form for file uploads.
Proxy?
Client::builder().proxy(reqwest::Proxy::http(url)?).
Retries?
Not built-in - see Retries page or reqwest-middleware crate.
mTLS?
Identity PKCS8 via rustls config on custom connector.
HTTP/2?
Enabled by default where server supports.
Mock tests?
wiremock or mockito against handler code using injected client.
DNS caching?
Handled by system resolver; custom hyper connector for advanced cases.
Rate limits from API?
Respect Retry-After header in retry layer.
Related
- TLS with rustls - HTTPS setup
- Retries, Timeouts & Backoff - Resilience
- hyper - Lower level
- Networking Best Practices - Client rules
- Web Backends Basics - Server counterpart
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.