How do I build HTTP clients with reqwest in Rust?
Walkthrough
The reqwest crate is the most popular HTTP client library in Rust. It provides a simple, blocking API for synchronous requests and a more powerful async API for asynchronous operations. Built on top of hyper and tokio, reqwest handles connection pooling, cookie storage, redirects, proxies, and TLS/SSL automatically. It's the go-to choice for making HTTP requests in Rust applications.
Key concepts:
- Blocking Client — simple synchronous API for scripts and simple apps
- Async Client — non-blocking API for high-performance applications
- Request Builder — fluent API for constructing requests with headers, body, etc.
- Response — access status, headers, body in various formats
- Error Handling — comprehensive error types for network, parse, and HTTP errors
Code Example
# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking"] }
tokio = { version = "1", features = ["full"] }use reqwest::blocking::get;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let body = get("https://httpbin.org/ip")?.text()?;
println!("Body: {}", body);
Ok(())
}Basic GET Requests
use reqwest::blocking::Client;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Simple GET request
let response = reqwest::blocking::get("https://httpbin.org/get")?;
println!("Status: {}", response.status());
println!("Headers: {:?}", response.headers());
// Read body as text
let text = response.text()?;
println!("Body: {}", text);
Ok(())
}Client with Configuration
use reqwest::blocking::Client;
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a configured client
let client = Client::builder()
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(5))
.user_agent("my-app/1.0")
.build()?;
// Make a request
let response = client.get("https://httpbin.org/get").send()?;
println!("Status: {}", response.status());
println!("Body: {}", response.text()?);
Ok(())
}JSON Requests and Responses
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
struct UserRequest {
name: String,
email: String,
}
#[derive(Debug, Deserialize)]
struct UserResponse {
id: u64,
name: String,
email: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Send JSON
let user = UserRequest {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let response = client
.post("https://httpbin.org/post")
.json(&user)
.send()?;
println!("Status: {}", response.status());
// Parse JSON response
let json: serde_json::Value = response.json()?;
println!("Response JSON: {:#}", json);
Ok(())
}POST Requests with Different Bodies
use reqwest::blocking::Client;
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// POST with JSON body
let response = client
.post("https://httpbin.org/post")
.json(&json!({ "key": "value" }))
.send()?;
println!("JSON POST: {}", response.status());
// POST with form data
let response = client
.post("https://httpbin.org/post")
.form(&[("username", "alice"), ("password", "secret")])
.send()?;
println!("Form POST: {}", response.status());
// POST with raw body
let response = client
.post("https://httpbin.org/post")
.body("raw body content")
.send()?;
println!("Raw POST: {}", response.status());
// POST with custom content type
let response = client
.post("https://httpbin.org/post")
.header("Content-Type", "application/xml")
.body("<xml>data</xml>")
.send()?;
println!("XML POST: {}", response.status());
Ok(())
}Custom Headers
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Method 1: Use header() method
let response = client
.get("https://httpbin.org/headers")
.header("X-Custom-Header", "custom-value")
.header(USER_AGENT, "my-app/1.0")
.send()?;
println!("Response: {}", response.status());
// Method 2: Use HeaderMap
let mut headers = HeaderMap::new();
headers.insert("X-Api-Key", HeaderValue::from_static("secret-key"));
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let response = client
.get("https://httpbin.org/headers")
.headers(headers)
.send()?;
println!("Response: {}", response.status());
// Authorization header
let token = "Bearer my-token";
let response = client
.get("https://httpbin.org/bearer")
.header(AUTHORIZATION, token)
.send()?;
println!("Auth response: {}", response.status());
Ok(())
}Query Parameters
use reqwest::blocking::Client;
use serde::Serialize;
#[derive(Serialize)]
struct SearchParams {
q: String,
page: u32,
per_page: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Method 1: Query method
let response = client
.get("https://httpbin.org/get")
.query(&[("key", "value"), ("foo", "bar")])
.send()?;
println!("URL: {}", response.url());
// Method 2: Struct with Serialize
let params = SearchParams {
q: "rust".to_string(),
page: 1,
per_page: 10,
};
let response = client
.get("https://httpbin.org/get")
.query(¶ms)
.send()?;
println!("URL: {}", response.url());
Ok(())
}Handling Responses
use reqwest::blocking::Client;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = client.get("https://httpbin.org/get").send()?;
// Status code
println!("Status: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or(""));
// Check if successful (2xx)
if response.status().is_success() {
println!("Request was successful");
}
// Check specific status codes
match response.status().as_u16() {
200 => println!("OK"),
201 => println!("Created"),
404 => println!("Not Found"),
500 => println!("Internal Server Error"),
_ => println!("Other status"),
}
// Headers
for (name, value) in response.headers() {
println!("Header: {} = {:?}", name, value);
}
// Get specific header
if let Some(content_type) = response.headers().get("content-type") {
println!("Content-Type: {:?}", content_type);
}
// Final URL (after redirects)
println!("Final URL: {}", response.url());
// Body formats
let response = client.get("https://httpbin.org/get").send()?;
let text = response.text()?; // As text
println!("Body length: {} chars", text.len());
Ok(())
}Error Handling
use reqwest::blocking::Client;
use reqwest::StatusCode;
fn fetch_url(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let response = Client::new().get(url).send()?;
match response.status() {
StatusCode::OK => Ok(response.text()?),
StatusCode::NOT_FOUND => Err("Resource not found".into()),
StatusCode::UNAUTHORIZED => Err("Unauthorized".into()),
StatusCode::INTERNAL_SERVER_ERROR => Err("Server error".into()),
status if status.is_client_error() => Err(format!("Client error: {}", status).into()),
status if status.is_server_error() => Err(format!("Server error: {}", status).into()),
_ => Ok(response.text()?),
}
}
fn main() {
match fetch_url("https://httpbin.org/status/404") {
Ok(body) => println!("Body: {}", body),
Err(e) => println!("Error: {}", e),
}
}Async Client
use reqwest::Client;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create async client
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
// Make async request
let response = client
.get("https://httpbin.org/get")
.send()
.await?;
println!("Status: {}", response.status());
println!("Body: {}", response.text().await?);
Ok(())
}Concurrent Requests
use reqwest::Client;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let urls = vec![
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/1",
];
let mut tasks = JoinSet::new();
for url in urls {
let client = client.clone();
let url = url.to_string();
tasks.spawn(async move {
let response = client.get(&url).send().await?;
println!("Completed: {}", url);
Ok::<_, reqwest::Error>(response.status())
});
}
while let Some(result) = tasks.join_next().await {
match result {
Ok(Ok(status)) => println!("Task completed: {}", status),
Ok(Err(e)) => println!("Request error: {}", e),
Err(e) => println!("Task error: {}", e),
}
}
Ok(())
}Streaming Response Body
use reqwest::Client;
use tokio::io::AsyncWriteExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Stream response to file
let response = client
.get("https://httpbin.org/bytes/1024")
.send()
.await?;
let mut file = tokio::fs::File::create("download.bin").await?;
let mut stream = response.bytes_stream();
use futures::StreamExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
}
println!("Downloaded to download.bin");
Ok(())
}Uploading Files
use reqwest::blocking::Client;
use std::fs::File;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Method 1: Upload file directly
let file = File::open("Cargo.toml")?;
let response = client
.post("https://httpbin.org/post")
.body(file)
.send()?;
println!("Upload status: {}", response.status());
// Method 2: Multipart form upload
let form = reqwest::blocking::multipart::Form::new()
.text("field", "value")
.file("file", "Cargo.toml")?;
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()?;
println!("Multipart status: {}", response.status());
Ok(())
}Cookies and Sessions
use reqwest::blocking::Client;
use reqwest::cookieJar;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with cookie store
let client = Client::builder()
.cookie_store(true)
.build()?;
// First request sets a cookie
let response = client
.get("https://httpbin.org/cookies/set?session=abc123")
.send()?;
println!("Set cookie: {}", response.status());
// Second request sends the cookie back
let response = client
.get("https://httpbin.org/cookies")
.send()?;
let cookies: serde_json::Value = response.json()?;
println!("Cookies: {:#}", cookies);
Ok(())
}Proxy Configuration
use reqwest::blocking::Client;
use reqwest::Proxy;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure proxy
let client = Client::builder()
.proxy(Proxy::all("http://proxy.example.com:8080")?)
.build()?;
// Or use environment variables
let client = Client::builder()
.use_sys_proxy() // Uses HTTP_PROXY, HTTPS_PROXY env vars
.build()?;
// Different proxies for different schemes
let client = Client::builder()
.proxy(Proxy::http("http://http-proxy:8080")?)
.proxy(Proxy::https("http://https-proxy:8080")?)
.build()?;
let response = client.get("https://httpbin.org/ip").send()?;
println!("Status: {}", response.status());
Ok(())
}Basic Authentication
use reqwest::blocking::Client;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Basic auth
let response = client
.get("https://httpbin.org/basic-auth/user/pass")
.basic_auth("user", Some("pass"))
.send()?;
println!("Basic auth: {}", response.status());
// Bearer auth
let token = "my-jwt-token";
let response = client
.get("https://httpbin.org/bearer")
.bearer_auth(token)
.send()?;
println!("Bearer auth: {}", response.status());
Ok(())
}Redirect Handling
use reqwest::blocking::Client;
use reqwest::redirect::Policy;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Follow redirects (default)
let client = Client::builder()
.redirect(Policy::limited(10))
.build()?;
let response = client
.get("https://httpbin.org/redirect/2")
.send()?;
println!("Final URL: {}", response.url());
println!("Status: {}", response.status());
// Don't follow redirects
let client = Client::builder()
.redirect(Policy::none())
.build()?;
let response = client
.get("https://httpbin.org/redirect/1")
.send()?;
println!("Status: {}", response.status()); // 302
// Get redirect location
if let Some(location) = response.headers().get("location") {
println!("Redirect to: {:?}", location);
}
Ok(())
}TLS/SSL Configuration
use reqwest::blocking::Client;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Accept invalid certificates (development only!)
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
// Note: In production, use proper certificate validation
// The default client validates certificates
let response = client
.get("https://httpbin.org/get")
.send()?;
println!("Status: {}", response.status());
Ok(())
}Retry Logic
use reqwest::blocking::Client;
use std::time::Duration;
use std::thread;
fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
let mut last_error = None;
for attempt in 0..max_retries {
match client.get(url).send() {
Ok(response) if response.status().is_success() => {
return Ok(response.text()?);
}
Ok(response) => {
last_error = Some(format!("HTTP {}", response.status()).into());
}
Err(e) => {
last_error = Some(Box::new(e));
}
}
if attempt < max_retries - 1 {
let delay = Duration::from_millis(100 * 2u64.pow(attempt));
println!("Retry {} in {:?}", attempt + 1, delay);
thread::sleep(delay);
}
}
Err(last_error.unwrap_or_else(|| "Unknown error".into()))
}
fn main() {
match fetch_with_retry("https://httpbin.org/get", 3) {
Ok(body) => println!("Success: {} bytes", body.len()),
Err(e) => println!("Failed after retries: {}", e),
}
}API Client Wrapper
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug)]
pub struct ApiClient {
client: Client,
base_url: String,
api_key: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
impl ApiClient {
pub fn new(base_url: &str, api_key: &str) -> Result<Self, reqwest::Error> {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
Ok(Self {
client,
base_url: base_url.to_string(),
api_key: api_key.to_string(),
})
}
pub fn get_user(&self, user_id: u64) -> Result<User, ApiError> {
let url = format!("{}/users/{}", self.base_url, user_id);
let response = self.client
.get(&url)
.header("X-Api-Key", &self.api_key)
.send()
.map_err(ApiError::Request)?;
if !response.status().is_success() {
return Err(ApiError::Http(response.status()));
}
response.json().map_err(ApiError::Parse)
}
pub fn list_users(&self) -> Result<Vec<User>, ApiError> {
let url = format!("{}/users", self.base_url);
let response = self.client
.get(&url)
.header("X-Api-Key", &self.api_key)
.send()
.map_err(ApiError::Request)?;
response.json().map_err(ApiError::Parse)
}
}
#[derive(Debug)]
pub enum ApiError {
Request(reqwest::Error),
Http(reqwest::StatusCode),
Parse(reqwest::Error),
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::Request(e) => write!(f, "Request error: {}", e),
ApiError::Http(status) => write!(f, "HTTP error: {}", status),
ApiError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for ApiError {}
fn main() {
let client = ApiClient::new("https://api.example.com", "secret-key").unwrap();
match client.get_user(1) {
Ok(user) => println!("User: {:?}", user),
Err(e) => println!("Error: {}", e),
}
}Async API Client
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct AsyncApiClient {
client: Client,
base_url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Post {
pub id: u64,
pub title: String,
pub body: String,
}
impl AsyncApiClient {
pub fn new(base_url: &str) -> Result<Self, reqwest::Error> {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
Ok(Self {
client,
base_url: base_url.to_string(),
})
}
pub async fn get_post(&self, id: u64) -> Result<Post, reqwest::Error> {
let url = format!("{}/posts/{}", self.base_url, id);
self.client.get(&url).send().await?.json().await
}
pub async fn list_posts(&self) -> Result<Vec<Post>, reqwest::Error> {
let url = format!("{}/posts", self.base_url);
self.client.get(&url).send().await?.json().await
}
pub async fn create_post(&self, post: &Post) -> Result<Post, reqwest::Error> {
let url = format!("{}/posts", self.base_url);
self.client
.post(&url)
.json(post)
.send().await?
.json().await
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = AsyncApiClient::new("https://jsonplaceholder.typicode.com")?;
// List posts
let posts = client.list_posts().await?;
println!("Found {} posts", posts.len());
// Get specific post
let post = client.get_post(1).await?;
println!("Post 1: {}", post.title);
// Create post
let new_post = Post {
id: 0,
title: "New Post".to_string(),
body: "This is the body".to_string(),
};
let created = client.create_post(&new_post).await?;
println!("Created post with ID: {}", created.id);
Ok(())
}Rate Limiting
use reqwest::Client;
use tokio::time::{sleep, Duration};
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Debug)]
struct RateLimitedClient {
client: Client,
last_request: Arc<Mutex<Option<tokio::time::Instant>>>,
min_interval: Duration,
}
impl RateLimitedClient {
fn new(min_interval: Duration) -> Self {
Self {
client: Client::new(),
last_request: Arc::new(Mutex::new(None)),
min_interval,
}
}
async fn get(&self, url: &str) -> Result<reqwest::Response, reqwest::Error> {
let mut last = self.last_request.lock().await;
if let Some(last_time) = *last {
let elapsed = last_time.elapsed();
if elapsed < self.min_interval {
sleep(self.min_interval - elapsed).await;
}
}
let response = self.client.get(url).send().await?;
*last = Some(tokio::time::Instant::now());
Ok(response)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = RateLimitedClient::new(Duration::from_millis(100));
for i in 0..5 {
let response = client.get("https://httpbin.org/get").await?;
println!("Request {}: {}", i, response.status());
}
Ok(())
}Summary
reqwest::blocking::get()for simple synchronous requestsreqwest::blocking::Clientfor configurable blocking clientsreqwest::Clientfor async clients (requires tokio runtime)- Use
.json()for JSON bodies and.json()on response for parsing - Use
.form()for URL-encoded form data - Use
.query()for URL query parameters - Add headers with
.header()or.headers() - Handle errors by checking
response.status()or usingStatus - Use
.text(),.json(),.bytes()for different body formats - Configure timeouts with
Client::builder().timeout() - Use
.multipart()for file uploads - Enable cookie storage with
.cookie_store(true) - Configure proxies with
Proxy::all()or.use_sys_proxy() - Use
.basic_auth()and.bearer_auth()for authentication - Clone async
Clientfor sharing across threads - Perfect for: API clients, web scraping, HTTP integrations, microservices
