How do I make HTTP requests in Rust?

Walkthrough

Reqwest is an ergonomic HTTP client built on Hyper. It provides both blocking and async APIs, automatic request/response handling, JSON serialization via serde, cookies, redirects, and TLS support. Reqwest simplifies HTTP interactions while offering powerful configuration options for production use.

Key features:

  1. Simple API — easy GET/POST/etc. methods
  2. Async and blocking — supports both paradigms
  3. JSON support — serialize/deserialize with serde
  4. Headers and auth — custom headers, bearer tokens, basic auth
  5. Timeouts and retries — configurable request policies
  6. TLS/SSL — HTTPS support via native-tls or rustls

Reqwest is the go-to HTTP client for most Rust applications.

Code Example

# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
use reqwest::Error;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Simple GET request
    let body = reqwest::get("https://httpbin.org/get").await?.text().await?;
    println!("Body: {}", body);
    
    Ok(())
}

Building Requests with Client

use reqwest::{Client, Error};
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Create a configured client
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .user_agent("my-app/1.0")
        .build()?;
    
    // GET request
    let response = client
        .get("https://httpbin.org/get")
        .send()
        .await?;
    
    println!("Status: {}", response.status());
    println!("Headers: {:?}", response.headers());
    
    // Response body as text
    let text = response.text().await?;
    println!("Body: {}", text);
    
    Ok(())
}

Query Parameters and Forms

use reqwest::{Client, Error};
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct HttpBinResponse {
    args: std::collections::HashMap<String, String>,
    url: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Query parameters
    let response = client
        .get("https://httpbin.org/get")
        .query(&[
            ("key", "value"),
            ("foo", "bar"),
        ])
        .send()
        .await?;
    
    let json: HttpBinResponse = response.json().await?;
    println!("URL: {}", json.url);
    println!("Args: {:?}", json.args);
    
    // URL-encoded form data
    let response = client
        .post("https://httpbin.org/post")
        .form(&[
            ("username", "alice"),
            ("password", "secret"),
        ])
        .send()
        .await?;
    
    println!("Form POST status: {}", response.status());
    
    Ok(())
}

JSON Requests and Responses

use reqwest::{Client, Error};
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize)]
struct CreateUser {
    name: String,
    email: String,
}
 
#[derive(Debug, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}
 
#[derive(Debug, Deserialize)]
struct UserResponse {
    data: User,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // POST JSON
    let new_user = CreateUser {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    let response = client
        .post("https://reqres.in/api/users")
        .json(&new_user)
        .send()
        .await?;
    
    println!("Created: {}", response.status());
    let created: User = response.json().await?;
    println!("User: {:?}", created);
    
    // GET JSON
    let response = client
        .get("https://reqres.in/api/users/2")
        .send()
        .await?;
    
    let user_response: UserResponse = response.json().await?;
    println!("Fetched user: {:?}", user_response.data);
    
    Ok(())
}

Headers and Authentication

use reqwest::{Client, Error, header};
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Custom headers
    let response = client
        .get("https://httpbin.org/headers")
        .header("X-Custom-Header", "custom-value")
        .header("Accept", "application/json")
        .send()
        .await?;
    
    println!("Status: {}", response.status());
    
    // Bearer token authentication
    let token = "your-api-token";
    let response = client
        .get("https://httpbin.org/bearer")
        .bearer_auth(token)
        .send()
        .await?;
    
    println!("Bearer auth status: {}", response.status());
    
    // Basic authentication
    let response = client
        .get("https://httpbin.org/basic-auth/user/pass")
        .basic_auth("user", Some("pass"))
        .send()
        .await?;
    
    println!("Basic auth status: {}", response.status());
    
    // Default headers for all requests
    let client_with_headers = Client::builder()
        .default_headers({
            let mut headers = header::HeaderMap::new();
            headers.insert("X-Api-Key", "my-api-key".parse().unwrap());
            headers.insert("Accept", "application/json".parse().unwrap());
            headers
        })
        .build()?;
    
    let response = client_with_headers
        .get("https://httpbin.org/headers")
        .send()
        .await?;
    
    println!("Default headers status: {}", response.status());
    
    Ok(())
}

Error Handling and Status Codes

use reqwest::{Client, Error, StatusCode};
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Check status code
    let response = client
        .get("https://httpbin.org/status/404")
        .send()
        .await?;
    
    match response.status() {
        StatusCode::OK => println!("Success!"),
        StatusCode::NOT_FOUND => println!("Not found"),
        StatusCode::INTERNAL_SERVER_ERROR => println!("Server error"),
        status => println!("Other status: {}", status),
    }
    
    // Error on non-success status
    let response = client
        .get("https://httpbin.org/get")
        .send()
        .await?;
    
    // .error_for_status() returns Err for 4xx/5xx
    let response = response.error_for_status()?;
    println!("Request succeeded: {}", response.status());
    
    // Chain error handling
    let result = client
        .get("https://httpbin.org/status/500")
        .send()
        .await?
        .error_for_status();
    
    match result {
        Ok(resp) => println!("Success: {}", resp.status()),
        Err(e) => {
            if e.is_status() {
                println!("HTTP error: {}", e.status().unwrap());
            } else if e.is_timeout() {
                println!("Request timed out");
            } else if e.is_connect() {
                println!("Connection failed");
            } else {
                println!("Error: {}", e);
            }
        }
    }
    
    Ok(())
}

Timeouts and Configuration

use reqwest::{Client, Error};
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::builder()
        .timeout(Duration::from_secs(5))
        .connect_timeout(Duration::from_secs(2))
        .pool_idle_timeout(Duration::from_secs(30))
        .pool_max_idle_per_host(5)
        .user_agent("my-app/1.0")
        .build()?;
    
    // Request-level timeout
    let response = client
        .get("https://httpbin.org/delay/1")
        .timeout(Duration::from_secs(2))
        .send()
        .await;
    
    match response {
        Ok(resp) => println!("Got response: {}", resp.status()),
        Err(e) if e.is_timeout() => println!("Request timed out!"),
        Err(e) => println!("Error: {}", e),
    }
    
    Ok(())
}

Multipart Form Data and File Uploads

use reqwest::{Client, Error, multipart};
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Multipart form with text and file
    let form = multipart::Form::new()
        .text("field1", "value1")
        .text("field2", "value2")
        .part("file", multipart::Part::bytes(b"file content".to_vec())
            .file_name("test.txt")
            .mime_str("text/plain")?);
    
    let response = client
        .post("https://httpbin.org/post")
        .multipart(form)
        .send()
        .await?;
    
    println!("Multipart upload status: {}", response.status());
    
    // Upload file from disk (requires "stream" feature)
    // let file = tokio::fs::File::open("example.txt").await?;
    // let part = multipart::Part::stream(reqwest::Body::from(file))
    //     .file_name("example.txt")
    //     .mime_str("text/plain")?;
    // let form = multipart::Form::new().part("file", part);
    
    Ok(())
}

Redirects and Cookies

use reqwest::{Client, Error, redirect};
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Control redirect behavior
    let client = Client::builder()
        .redirect(redirect::Policy::limited(5))
        .build()?;
    
    let response = client
        .get("https://httpbin.org/redirect/2")
        .send()
        .await?;
    
    println!("Final URL: {}", response.url());
    println!("Status: {}", response.status());
    
    // Disable redirects
    let no_redirect_client = Client::builder()
        .redirect(redirect::Policy::none())
        .build()?;
    
    let response = no_redirect_client
        .get("https://httpbin.org/redirect/1")
        .send()
        .await?;
    
    println!("No redirect status: {}", response.status());
    
    // Cookie store (requires "cookies" feature)
    // let client = Client::builder()
    //     .cookie_store(true)
    //     .build()?;
    
    Ok(())
}

Blocking Client (Synchronous)

# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
use reqwest::blocking::Client;
use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct IpInfo {
    ip: String,
}
 
fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Simple blocking GET
    let body = client
        .get("https://httpbin.org/ip")
        .send()?;
    
    let ip_info: IpInfo = body.json()?;
    println!("Your IP: {}", ip_info.ip);
    
    // POST with JSON
    let response = client
        .post("https://httpbin.org/post")
        .json(&serde_json::json!({ "key": "value" }))
        .send()?;
    
    println!("Status: {}", response.status());
    
    Ok(())
}

Concurrent Requests

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .build()?;
    
    let urls = vec![
        "https://httpbin.org/delay/1",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/delay/1",
    ];
    
    // Fetch all URLs concurrently
    let handles: Vec<_> = urls
        .into_iter()
        .map(|url| {
            let client = client.clone();
            tokio::spawn(async move {
                let resp = client.get(url).send().await?;
                println!("Fetched {} -> {}", url, resp.status());
                Ok::<_, reqwest::Error>(resp)
            })
        })
        .collect();
    
    // Wait for all requests
    for handle in handles {
        handle.await??;
    }
    
    // Using futures::join! (requires futures crate)
    // let (r1, r2, r3) = tokio::join!(
    //     client.get("https://httpbin.org/get").send(),
    //     client.get("https://httpbin.org/ip").send(),
    //     client.get("https://httpbin.org/headers").send(),
    // );
    
    Ok(())
}

Summary

  • Use reqwest::get(url) for simple one-off GET requests
  • Create a Client for reusable connections and configuration
  • Call .text(), .json(), or .bytes() on response to get body
  • Use .query(&[...]) for URL query parameters
  • Use .form(&[...]) for URL-encoded form data
  • Use .json(&struct) for JSON body (requires serde feature)
  • Add headers with .header("name", "value") or .default_headers() on client
  • Authenticate with .bearer_auth(token) or .basic_auth(user, pass)
  • Check status with .status(); use .error_for_status() to error on non-2xx
  • Configure timeouts with .timeout() on client or individual requests
  • Use .multipart() for file uploads and multipart forms
  • Control redirects with .redirect() policy
  • Enable blocking feature for synchronous reqwest::blocking::Client
  • Clone Client for concurrent requests across threads
  • Handle errors with .is_timeout(), .is_status(), .is_connect() methods