reqwest
Easy-to-use and powerful HTTP client for Rust. Complete async/await support, high performance based on Hyper, rich feature set. Provides automatic JSON processing, authentication, proxy, redirects, cookie management, and file upload capabilities. Most popular HTTP client in Rust ecosystem.
Library
Reqwest
Overview
Reqwest is an "ergonomic HTTP client for Rust" that has established itself as the de facto standard high-level HTTP client library in the Rust ecosystem. Designed with emphasis on "ease of use," it provides both asynchronous (async) and blocking APIs. Supporting comprehensive features for enterprise-level HTTP communication requirements including connection pooling, TLS, redirects, cookies, authentication systems, and JSON/form processing, it has been widely adopted as an essential library for Rust web application development.
Details
Reqwest 2025 edition continues to maintain its solid position as the definitive Rust HTTP client solution. By providing both non-blocking I/O (async API) built on the Tokio runtime and traditional blocking I/O (blocking API), it supports diverse programming models. Built on top of low-level libraries like hyper, the high-level API enables complex HTTP communication to be implemented simply and intuitively. It provides integrated comprehensive functionality necessary for modern web application development including connection pooling, TLS/SSL configuration, proxy support, redirect handling, session management, and WASM support.
Key Features
- Dual API Design: Flexible usage supporting both async/await and blocking approaches
- Comprehensive Authentication Support: Complete support for Basic, Bearer, and proxy authentication
- Multi-format Support: JSON, URL-encoded, and multipart form processing
- Advanced TLS Control: Multiple TLS backend selection and detailed SSL configuration
- Efficient Connection Management: Automatic connection pooling and session optimization
- Complete WASM Support: Integration with browser environment Fetch API
Pros and Cons
Pros
- Overwhelming adoption rate in Rust ecosystem with abundant learning resources
- High development efficiency through ergonomic high-level APIs
- Flexible programming models supporting both async/await and blocking
- Enterprise-level support through comprehensive HTTP features (TLS, authentication, proxy, etc.)
- Excellent performance and efficient connection pooling
- Integration with frontend development through WASM support
Cons
- Large default dependencies due to rich features, increasing binary size
- Limited functionality in WASM environment (TLS, cookies, blocking API restrictions)
- Potential panics when using blocking API within async runtime
- Need for direct hyper usage when low-level control is required
- Potential malfunction with incorrect feature flag configuration
- Requires understanding of async/await during initial learning
Reference Pages
Code Examples
Installation and Basic Setup
# Cargo.toml - Basic configuration
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
# Full feature enabled version
[dependencies]
reqwest = { version = "0.12", features = ["json", "multipart", "stream", "gzip", "brotli"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
# Blocking API only
[dependencies]
reqwest = { version = "0.12", features = ["blocking", "json"] }
# WASM support version
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
Basic Requests (GET/POST/PUT/DELETE)
use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: Option<u32>,
name: String,
email: String,
age: u32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Basic GET request (shortcut)
let response = reqwest::get("https://api.example.com/users")
.await?;
println!("Status: {}", response.status());
println!("Headers: {:#?}", response.headers());
// JSON response processing
let body = response.text().await?;
println!("Body: {}", body);
// GET request with structured data
let users: Vec<User> = reqwest::get("https://api.example.com/users")
.await?
.json()
.await?;
println!("Users: {:#?}", users);
// Create Client instance (recommended for multiple requests)
let client = reqwest::Client::new();
// GET request with query parameters
let users = client
.get("https://api.example.com/users")
.query(&[("page", "1"), ("limit", "10"), ("sort", "created_at")])
.send()
.await?
.json::<Vec<User>>()
.await?;
// POST request (sending JSON)
let new_user = User {
id: None,
name: "John Doe".to_string(),
email: "[email protected]".to_string(),
age: 30,
};
let created_user = client
.post("https://api.example.com/users")
.header("Authorization", "Bearer your-token")
.json(&new_user)
.send()
.await?
.json::<User>()
.await?;
println!("Created user: {:#?}", created_user);
// POST request (sending form data)
let mut form_data = HashMap::new();
form_data.insert("username", "testuser");
form_data.insert("password", "secret123");
let login_response = client
.post("https://api.example.com/login")
.form(&form_data)
.send()
.await?;
// PUT request (data update)
let user_id = created_user.id.unwrap();
let updated_data = User {
id: Some(user_id),
name: "Jane Doe".to_string(),
email: "[email protected]".to_string(),
age: 31,
};
let updated_user = client
.put(&format!("https://api.example.com/users/{}", user_id))
.header("Authorization", "Bearer your-token")
.json(&updated_data)
.send()
.await?
.json::<User>()
.await?;
// DELETE request
let delete_response = client
.delete(&format!("https://api.example.com/users/{}", user_id))
.header("Authorization", "Bearer your-token")
.send()
.await?;
if delete_response.status().is_success() {
println!("User deleted successfully");
}
Ok(())
}
// Blocking API usage example
fn blocking_example() -> Result<(), Box<dyn std::error::Error>> {
// Blocking client (non-async environment)
let client = reqwest::blocking::Client::new();
let response = client
.get("https://api.example.com/users")
.header("User-Agent", "MyRustApp/1.0")
.send()?;
let users: Vec<User> = response.json()?;
println!("Users: {:#?}", users);
Ok(())
}
Advanced Configuration and Customization (Authentication, Timeout, TLS, etc.)
use reqwest::{Client, ClientBuilder, header, Certificate};
use std::time::Duration;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Custom client builder configuration
let client = ClientBuilder::new()
.timeout(Duration::from_secs(30)) // Overall timeout
.connect_timeout(Duration::from_secs(10)) // Connection timeout
.tcp_keepalive(Duration::from_secs(60)) // TCP KeepAlive
.pool_idle_timeout(Duration::from_secs(90)) // Connection pool idle time
.pool_max_idle_per_host(10) // Max idle connections per host
.user_agent("MyRustApp/1.0 (Reqwest)") // User-Agent header
.gzip(true) // Enable gzip compression
.brotli(true) // Enable Brotli compression
.deflate(true) // Enable Deflate compression
.cookie_store(true) // Enable cookie store
.redirect(reqwest::redirect::Policy::limited(10)) // Redirect limit
.build()?;
// Custom header configuration
let mut headers = header::HeaderMap::new();
headers.insert("X-API-Version", "v2".parse()?);
headers.insert("Accept", "application/json".parse()?);
headers.insert("Accept-Language", "en-US,ja-JP".parse()?);
headers.insert("X-Request-ID", "req-12345".parse()?);
let response = client
.get("https://api.example.com/data")
.headers(headers)
.send()
.await?;
// Basic authentication
let response = client
.get("https://api.example.com/private")
.basic_auth("username", Some("password"))
.send()
.await?;
// Bearer Token authentication
let response = client
.get("https://api.example.com/protected")
.bearer_auth("your-jwt-token")
.send()
.await?;
// Proxy configuration
let proxy = reqwest::Proxy::http("http://proxy.example.com:8080")?
.basic_auth("proxy_user", "proxy_pass");
let client_with_proxy = ClientBuilder::new()
.proxy(proxy)
.build()?;
// TLS/SSL detailed configuration
let cert = fs::read("client.crt")?;
let cert = Certificate::from_pem(&cert)?;
let tls_client = ClientBuilder::new()
.add_root_certificate(cert) // Custom CA certificate
.danger_accept_invalid_certs(false) // Enable certificate verification
.danger_accept_invalid_hostnames(false) // Enable hostname verification
.min_tls_version(reqwest::tls::Version::TLS_1_2) // Minimum TLS version
.use_rustls_tls() // Use Rustls TLS
.build()?;
// Detailed timeout control
let response = client
.get("https://api.example.com/slow-endpoint")
.timeout(Duration::from_secs(60)) // Timeout for this request only
.send()
.await?;
Ok(())
}
// Multipart form file upload
async fn file_upload_example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Create multipart form
let form = reqwest::multipart::Form::new()
.text("description", "Upload file")
.text("category", "documents")
.file("file", "/path/to/document.pdf")
.await?;
let response = client
.post("https://api.example.com/upload")
.bearer_auth("your-token")
.multipart(form)
.send()
.await?;
let upload_result: serde_json::Value = response.json().await?;
println!("Upload result: {:#?}", upload_result);
Ok(())
}
// Streaming processing
async fn streaming_example() -> Result<(), Box<dyn std::error::Error>> {
use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
let client = Client::new();
let response = client
.get("https://api.example.com/large-file.zip")
.send()
.await?;
// Streaming download
let mut file = tokio::fs::File::create("downloaded_file.zip").await?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
}
println!("File downloaded successfully");
Ok(())
}
Error Handling and Retry Functionality
use reqwest::{Client, Error, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug)]
enum APIError {
NetworkError(Error),
HTTPError(StatusCode),
TimeoutError,
UnauthorizedError,
NotFoundError,
ServerError,
}
impl From<Error> for APIError {
fn from(err: Error) -> Self {
if err.is_timeout() {
APIError::TimeoutError
} else {
APIError::NetworkError(err)
}
}
}
// Comprehensive error handling
async fn safe_request(
client: &Client,
url: &str,
) -> Result<reqwest::Response, APIError> {
let response = client.get(url).send().await?;
match response.status() {
StatusCode::OK => Ok(response),
StatusCode::UNAUTHORIZED => Err(APIError::UnauthorizedError),
StatusCode::NOT_FOUND => Err(APIError::NotFoundError),
status if status.is_server_error() => Err(APIError::ServerError),
status => Err(APIError::HTTPError(status)),
}
}
// Request with retry functionality
async fn request_with_retry<T>(
client: &Client,
url: &str,
max_retries: u32,
backoff_factor: u64,
) -> Result<T, APIError>
where
T: serde::de::DeserializeOwned,
{
for attempt in 0..=max_retries {
match safe_request(client, url).await {
Ok(response) => {
return response
.json::<T>()
.await
.map_err(|e| APIError::NetworkError(e));
}
Err(APIError::ServerError) | Err(APIError::TimeoutError) => {
if attempt == max_retries {
return Err(APIError::ServerError);
}
let wait_time = backoff_factor * 2_u64.pow(attempt);
println!("Retry {} / {} - retrying in {} seconds", attempt + 1, max_retries, wait_time);
sleep(Duration::from_secs(wait_time)).await;
}
Err(e) => return Err(e), // Permanent error
}
}
Err(APIError::ServerError)
}
// Detailed error handling example
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Concise error handling using error_for_status method
let response = client
.get("https://api.example.com/data")
.send()
.await?
.error_for_status()?; // Convert 4xx/5xx to error
let data: serde_json::Value = response.json().await?;
// Custom error handling
match request_with_retry::<Vec<serde_json::Value>>(&client, "https://api.example.com/users", 3, 1).await {
Ok(users) => println!("Users retrieved successfully: {} items", users.len()),
Err(APIError::UnauthorizedError) => {
println!("Authentication error: Please check your token");
}
Err(APIError::NotFoundError) => {
println!("Resource not found");
}
Err(APIError::TimeoutError) => {
println!("Request timeout: Please check network conditions");
}
Err(APIError::ServerError) => {
println!("Server error: Please try again later");
}
Err(APIError::HTTPError(status)) => {
println!("Unexpected HTTP error: {}", status);
}
Err(APIError::NetworkError(err)) => {
println!("Network error: {}", err);
}
}
Ok(())
}
// Circuit breaker implementation example
use std::sync::{Arc, atomic::{AtomicU32, Ordering}};
struct CircuitBreaker {
failure_count: Arc<AtomicU32>,
failure_threshold: u32,
is_open: Arc<AtomicU32>,
}
impl CircuitBreaker {
fn new(failure_threshold: u32) -> Self {
Self {
failure_count: Arc::new(AtomicU32::new(0)),
failure_threshold,
is_open: Arc::new(AtomicU32::new(0)),
}
}
async fn call<F, Fut, T>(&self, f: F) -> Result<T, APIError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T, APIError>>,
{
if self.is_open.load(Ordering::Relaxed) == 1 {
return Err(APIError::ServerError);
}
match f().await {
Ok(result) => {
self.failure_count.store(0, Ordering::Relaxed);
self.is_open.store(0, Ordering::Relaxed);
Ok(result)
}
Err(e) => {
let failures = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
if failures >= self.failure_threshold {
self.is_open.store(1, Ordering::Relaxed);
println!("Circuit breaker activated");
}
Err(e)
}
}
}
}
Concurrent Processing and Performance Optimization
use reqwest::Client;
use tokio::task::JoinSet;
use futures_util::{future::join_all, stream::{self, StreamExt}};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Arc::new(Client::new());
// Concurrent fetching of multiple URLs (using join_all)
let urls = vec![
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments",
"https://api.example.com/categories",
];
let requests = urls.iter().map(|url| {
let client = client.clone();
let url = url.to_string();
async move {
client.get(&url).send().await?.text().await
}
});
let results = join_all(requests).await;
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(body) => println!("URL {} success: {} characters", i, body.len()),
Err(e) => println!("URL {} error: {}", i, e),
}
}
// Concurrent processing using JoinSet
let mut join_set = JoinSet::new();
for (i, url) in urls.iter().enumerate() {
let client = client.clone();
let url = url.to_string();
join_set.spawn(async move {
let result = client.get(&url).send().await?.json::<serde_json::Value>().await?;
Ok::<(usize, serde_json::Value), reqwest::Error>((i, result))
});
}
while let Some(result) = join_set.join_next().await {
match result {
Ok(Ok((index, data))) => println!("Task {} completed: {} keys", index, data.as_object().map_or(0, |o| o.len())),
Ok(Err(e)) => println!("Request error: {}", e),
Err(e) => println!("Task error: {}", e),
}
}
// Controlled concurrent execution using stream processing
let concurrent_limit = 5;
let results: Vec<_> = stream::iter(urls)
.map(|url| {
let client = client.clone();
async move {
client.get(url).send().await?.text().await
}
})
.buffer_unordered(concurrent_limit) // Limit concurrent execution
.collect()
.await;
let successful_count = results.iter().filter(|r| r.is_ok()).count();
println!("Success: {} / {} requests", successful_count, urls.len());
Ok(())
}
// Pagination-aware full data retrieval
async fn fetch_all_pages<T>(
client: &Client,
base_url: &str,
auth_token: &str,
) -> Result<Vec<T>, Box<dyn std::error::Error>>
where
T: serde::de::DeserializeOwned,
{
let mut all_data = Vec::new();
let mut page = 1;
let per_page = 100;
loop {
let response = client
.get(base_url)
.bearer_auth(auth_token)
.query(&[("page", page), ("per_page", per_page)])
.send()
.await?
.json::<serde_json::Value>()
.await?;
let items = response["items"].as_array()
.ok_or("Invalid response format")?;
if items.is_empty() {
break;
}
for item in items {
let typed_item: T = serde_json::from_value(item.clone())?;
all_data.push(typed_item);
}
println!("Page {} completed: {} items", page, items.len());
// Check if next page exists
if response["has_more"].as_bool().unwrap_or(false) == false {
break;
}
page += 1;
// Wait to reduce API load
tokio::time::sleep(Duration::from_millis(100)).await;
}
println!("Total items retrieved: {}", all_data.len());
Ok(all_data)
}
// Connection pool customization
fn create_optimized_client() -> Result<Client, reqwest::Error> {
ClientBuilder::new()
.pool_max_idle_per_host(20) // Max idle connections per host
.pool_idle_timeout(Duration::from_secs(120)) // Idle timeout
.tcp_keepalive(Duration::from_secs(60)) // TCP KeepAlive
.tcp_nodelay(true) // Disable Nagle algorithm
.http2_prior_knowledge() // Prefer HTTP/2
.http2_keep_alive_interval(Duration::from_secs(30))
.http2_keep_alive_timeout(Duration::from_secs(10))
.build()
}
WASM Integration and Practical Examples
// WASM environment usage example
#[cfg(target_arch = "wasm32")]
mod wasm_example {
use reqwest::Client;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::console;
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
spawn_local(async {
if let Err(e) = fetch_data().await {
console::log_1(&format!("Error: {:?}", e).into());
}
});
}
async fn fetch_data() -> Result<(), reqwest::Error> {
let client = Client::new();
// GET request in WASM environment (using Fetch API)
let response = client
.get("https://api.example.com/data")
.header("Accept", "application/json")
.send()
.await?;
let data: serde_json::Value = response.json().await?;
console::log_1(&format!("Data: {:?}", data).into());
Ok(())
}
// Browser CORS handling
async fn cors_request() -> Result<(), reqwest::Error> {
let client = Client::new();
let response = client
.post("https://api.example.com/data")
.header("Content-Type", "application/json")
.header("Access-Control-Allow-Origin", "*")
.json(&serde_json::json!({"message": "Hello from WASM"}))
.send()
.await?;
console::log_1(&format!("Status: {}", response.status()).into());
Ok(())
}
}
// Mocking for integration tests
#[cfg(test)]
mod tests {
use super::*;
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};
#[tokio::test]
async fn test_api_integration() {
// Start mock server
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!([
{"id": 1, "name": "Alice", "email": "[email protected]", "age": 30},
{"id": 2, "name": "Bob", "email": "[email protected]", "age": 25}
])))
.mount(&mock_server)
.await;
let client = Client::new();
let url = format!("{}/users", mock_server.uri());
let users: Vec<serde_json::Value> = client
.get(&url)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(users.len(), 2);
assert_eq!(users[0]["name"], "Alice");
}
}