reqwest
reqwest
Level 16 — Ecosystem & Tooling The standard, high-level asynchronous HTTP client library in Rust — built on
tokioandhyper, supporting JSON serialization/deserialization viaserde, connection pooling, TLS encryption, cookies, proxies, and streaming responses.
1. Prerequisites
tokio— Asynchronous runtime underlyingreqwest.serde— Used byreqwestfor JSON response parsing (.json::<T>()).async/.await— Asynchronous network request handling.
2. Term Category
Rust Ecosystem Crate (asynchronous HTTP client library): reqwest is the de facto standard HTTP client for Rust. Similar to axios or fetch in JavaScript, reqwest provides an ergonomic, feature-complete HTTP client interface for sending GET, POST, PUT, DELETE requests asynchronously.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Low-level HTTP libraries (like hyper) require manually managing HTTP state machines, header buffers, TLS sockets, and chunked transfer decoding.
reqwest provides a clean, high-level client abstraction:
- Connection Pooling: Reuses TCP connections automatically via
reqwest::Client. - Automatic JSON Parsing: Integrates with
serdeso.json::<MyStruct>().awaitdeserializes HTTP response bodies directly into Rust types. - TLS out-of-the-box: Supports
native-tlsorrustls(pure Rust TLS).
(2) Code Examples
Asynchronous HTTP GET Request & Serde JSON Parsing
use reqwest;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct GitHubUser {
login: String,
public_repos: u32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Instantiate reusable client (enables HTTP connection pooling)
let client = reqwest::Client::new();
// Send asynchronous GET request with custom User-Agent header
let response: GitHubUser = client
.get("https://api.github.com/users/octocat")
.header("User-Agent", "Rust-Reqwest-Client")
.send()
.await?
.json::<GitHubUser>()
.await?;
println!("GitHub User: {}, Repos: {}", response.login, response.public_repos);
Ok(())
}
4. Common Mistakes & Pitfalls
Mistake 2: Forgetting Timeout Configurations on Asynchronous HTTP Requests
The mistake: Issuing requests using reqwest::get without setting a timeout.
Why it's wrong: If a remote server hangs, the HTTP request hangs indefinitely, leaking async tasks and worker resources.
Fix: Build a client with .timeout(Duration::from_secs(10)) explicit limits.
Mistake 3: Swallowing Response Status Errors Without Calling .error_for_status()
The mistake: Reading response bodies directly without checking res.status().is_success().
Why it's wrong: HTTP 4xx/5xx responses return valid HTML/JSON error bodies that will fail deserialization with confusing error messages.
Fix: Call res.error_for_status()? to convert HTTP error codes into reqwest::Error.
Mistake 1: Re-instantiating reqwest::Client::new() on Every Request
The mistake: Calling reqwest::Client::new() inside a loop for every HTTP request.
Why it's wrong: reqwest::Client holds a connection pool. Creating a new client instance every time drops the connection pool, forcing a full TCP handshake and TLS negotiation on every request.
Fix:
// Create one `Client` instance and reuse it across requests:
let client = reqwest::Client::new();
5. Practice Exercises
Exercise 1: Asynchronous HTTP GET Request with Query Parameters and Headers
Scenario: Construct an asynchronous function fetch_weather_report that uses reqwest::Client to send an HTTP GET request to a weather service API ("{base_url}/weather"). The function must attach query parameters (q for city, appid for API key, and units set to "metric"), configure standard HTTP headers (User-Agent and Accept), handle HTTP error status codes, and deserialize the JSON response into a strongly-typed WeatherResponse struct. Write unit tests with assertions (assert_eq!, assert!) verifying the JSON deserialization logic.
Answer
Implementation
use reqwest::header::{ACCEPT, USER_AGENT};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct MainData {
pub temp: f64,
pub humidity: u32,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct WeatherCondition {
pub description: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct WeatherResponse {
pub name: String,
pub main: MainData,
pub weather: Vec<WeatherCondition>,
}
pub async fn fetch_weather_report(
client: &reqwest::Client,
base_url: &str,
city: &str,
api_key: &str,
) -> Result<WeatherResponse, reqwest::Error> {
let url = format!("{}/weather", base_url);
let response = client
.get(&url)
.query(&[("q", city), ("appid", api_key), ("units", "metric")])
.header(USER_AGENT, "Rust-Weather-App/1.0")
.header(ACCEPT, "application/json")
.send()
.await?
.error_for_status()?
.json::<WeatherResponse>()
.await?;
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_weather_deserialization() {
let raw_json = r#"{
"name": "Tokyo",
"main": {
"temp": 24.5,
"humidity": 65
},
"weather": [
{ "description": "clear sky" }
]
}"#;
let parsed: WeatherResponse =
serde_json::from_str(raw_json).expect("Failed to deserialize JSON payload");
assert_eq!(parsed.name, "Tokyo");
assert_eq!(parsed.main.temp, 24.5);
assert_eq!(parsed.main.humidity, 65);
assert_eq!(parsed.weather.len(), 1);
assert_eq!(parsed.weather[0].description, "clear sky");
assert!(parsed.main.temp > 0.0);
}
}
Technical Explanation
- Query String Serialization:
.query(&[("q", city), ...])converts key-value slices into URL-encoded query strings automatically. - Header Injection: Custom headers are set using standard
reqwest::headerconstants (USER_AGENT,ACCEPT), guaranteeing valid HTTP header naming. - Status Check via
.error_for_status()?: Converts non-2xx HTTP response codes (e.g. 404 Not Found, 500 Server Error) directly into areqwest::ErrorResult variant before attempting deserialization. - Asynchronous JSON Parsing:
.json::<T>().awaitstreams the response body asynchronously into memory and deserializes it viaserde_json.
Exercise 2: Authenticated JSON POST Request with Custom Client Builder & Timeout
Scenario: Build an API client module for user registration. Create a function create_configured_client that builds a reqwest::Client configured with custom request timeouts and TCP keep-alive settings. Then implement an asynchronous register_user function that sends an HTTP POST request containing a JSON body (CreateUserPayload) and a Bearer token Authorization header, returning a parsed UserCreatedResponse. Write unit tests with assert_eq! and assert! to test client setup, payload serialization, and response parsing.
Answer
Implementation
use std::time::Duration;
use reqwest::header::{AUTHORIZATION, USER_AGENT};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct CreateUserPayload {
pub username: String,
pub email: String,
pub role: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct UserCreatedResponse {
pub id: u64,
pub username: String,
pub status: String,
pub created_at: String,
}
pub fn create_configured_client(timeout_secs: u64) -> Result<reqwest::Client, reqwest::Error> {
reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.tcp_keepalive(Duration::from_secs(60))
.user_agent("UserService/2.0")
.build()
}
pub async fn register_user(
client: &reqwest::Client,
endpoint: &str,
bearer_token: &str,
payload: &CreateUserPayload,
) -> Result<UserCreatedResponse, reqwest::Error> {
let response = client
.post(endpoint)
.header(AUTHORIZATION, format!("Bearer {}", bearer_token))
.json(payload)
.send()
.await?
.error_for_status()?
.json::<UserCreatedResponse>()
.await?;
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_builder_configuration() {
let client_res = create_configured_client(5);
assert!(client_res.is_ok());
}
#[test]
fn test_payload_serialization_and_deserialization() {
let payload = CreateUserPayload {
username: String::from("alice_dev"),
email: String::from("alice@example.com"),
role: String::from("admin"),
};
let serialized = serde_json::to_string(&payload).expect("Serialization failed");
assert!(serialized.contains("alice_dev"));
assert!(serialized.contains("alice@example.com"));
let mock_response_json = r#"{
"id": 1042,
"username": "alice_dev",
"status": "created",
"created_at": "2026-07-30T10:00:00Z"
}"#;
let response: UserCreatedResponse =
serde_json::from_str(mock_response_json).expect("Deserialization failed");
assert_eq!(response.id, 1042);
assert_eq!(response.username, "alice_dev");
assert_eq!(response.status, "created");
assert_eq!(response.created_at, "2026-07-30T10:00:00Z");
assert!(response.id > 0);
}
}
Technical Explanation
- Client Customization:
reqwest::Client::builder()configures connection-level parameters (e.g. timeouts, TLS, connection pools) once per application lifetime. - JSON Request Body: Calling
.json(&payload)on aRequestBuilderserializes the Rust value usingserde_jsonand automatically sets theContent-Type: application/jsonHTTP header. - Authorization Header: Formats HTTP standard
Authorization: Bearer <token>for OAuth2/JWT secured endpoints. - Unit Verification: Tests verify request body serialization and mock response deserialization with field assertions.
Exercise 3: Concurrent Batch HTTP Requests with Connection Pool Reuse
Scenario: When retrieving data from multiple endpoints, sequential HTTP requests introduce cumulative network latency. Implement an asynchronous function fetch_posts_batch that receives a shared &reqwest::Client, a base URL, and a slice of post IDs (&[u32]). Spawn concurrent tasks using tokio::spawn to fetch posts in parallel while reusing the client's internal connection pool, returning Result<Vec<Post>, String>. Write unit tests with assertions (assert_eq!, assert!) validating batch response parsing and struct field values.
Answer
Implementation
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Post {
pub id: u32,
pub user_id: u32,
pub title: String,
pub body: String,
}
pub async fn fetch_posts_batch(
client: &reqwest::Client,
base_url: &str,
post_ids: &[u32],
) -> Result<Vec<Post>, String> {
let mut handles = Vec::new();
for &id in post_ids {
// reqwest::Client uses an internal Arc, so clone is very cheap
let client_clone = client.clone();
let url = format!("{}/posts/{}", base_url, id);
let handle = tokio::spawn(async move {
let res = client_clone
.get(&url)
.send()
.await
.map_err(|e| e.to_string())?
.error_for_status()
.map_err(|e| e.to_string())?
.json::<Post>()
.await
.map_err(|e| e.to_string())?;
Ok::<Post, String>(res)
});
handles.push(handle);
}
let mut posts = Vec::new();
for handle in handles {
match handle.await {
Ok(Ok(post)) => posts.push(post),
Ok(Err(err_msg)) => return Err(format!("HTTP Request failed: {}", err_msg)),
Err(join_err) => return Err(format!("Task panicked: {}", join_err)),
}
}
Ok(posts)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_post_batch_deserialization() {
let raw_json_1 = r#"{
"id": 1,
"user_id": 10,
"title": "Async Rust in Practice",
"body": "Reqwest enables high-performance concurrent networking."
}"#;
let raw_json_2 = r#"{
"id": 2,
"user_id": 10,
"title": "Serde JSON Integration",
"body": "Deserialization is zero-cost and type-safe."
}"#;
let post1: Post = serde_json::from_str(raw_json_1).expect("Failed post 1 parsing");
let post2: Post = serde_json::from_str(raw_json_2).expect("Failed post 2 parsing");
let batch = vec![post1, post2];
assert_eq!(batch.len(), 2);
assert_eq!(batch[0].id, 1);
assert_eq!(batch[0].user_id, 10);
assert_eq!(batch[0].title, "Async Rust in Practice");
assert_eq!(batch[1].id, 2);
assert_eq!(batch[1].user_id, 10);
assert_eq!(batch[1].title, "Serde JSON Integration");
assert!(batch.iter().all(|p| p.user_id == 10));
}
}
Technical Explanation
- Cheap
ClientCloning (ArcAbstraction):reqwest::Clientwraps an internal reference-counted handle (Arc). Cloning a client instance is an operation that increments the reference counter without reallocating connection pools or socket state. - Concurrency with
tokio::spawn: Spawning tasks allows network requests to run concurrently across available Tokio worker threads, significantly reducing round-trip latency for batch operations. - Error Handling & Task Joining: The outer loop waits on task
JoinHandleinstances, separating async task runtime panics from domain-specific HTTP/serialization errors.
6. Related Terms
- None!
7. Key Takeaways
reqwestis the standard async HTTP client for Rust.- Built on
tokioandhyper, supporting connection pooling, TLS, and automaticserdeJSON integration. - Reuse a single
reqwest::Clientinstance across requests to maintain connection pools.