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:

  1. Blocking Client — simple synchronous API for scripts and simple apps
  2. Async Client — non-blocking API for high-performance applications
  3. Request Builder — fluent API for constructing requests with headers, body, etc.
  4. Response — access status, headers, body in various formats
  5. 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(&params)
        .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 requests
  • reqwest::blocking::Client for configurable blocking clients
  • reqwest::Client for 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 using Status
  • 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 Client for sharing across threads
  • Perfect for: API clients, web scraping, HTTP integrations, microservices