How do I make HTTP requests in Rust?
Walkthrough
The reqwest crate is a popular, ergonomic HTTP client for Rust. It provides both blocking and async APIs, supports HTTPS, cookies, JSON serialization, multipart forms, and more. Built on top of hyper and tokio, reqwest handles the complexity of HTTP while offering a simple, chainable interface. It's the de-facto standard for making HTTP requests in Rust applications.
Key features:
- Async and blocking clients — choose based on your needs
- Request builders — chainable methods to configure requests
- Automatic JSON — serialize/deserialize with serde
- Headers and cookies — full HTTP feature support
- TLS/HTTPS — secure connections by default
- Timeouts and redirects — configurable behavior
Reqwest integrates seamlessly with Tokio for async applications.
Code Example
# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }use reqwest::Error;
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[tokio::main]
async fn main() -> Result<(), Error> {
// Simple GET request
let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
let user: User = response.json().await?;
println!("User: {} ({})", user.name, user.email);
Ok(())
}Basic Requests
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!("GET response: {}", body);
// Get response details
let response = reqwest::get("https://httpbin.org/get").await?;
println!("Status: {}", response.status());
println!("Headers: {:?}", response.headers());
// Check status before proceeding
let response = reqwest::get("https://httpbin.org/status/404").await?;
if response.status().is_success() {
let body = response.text().await?;
println!("Success: {}", body);
} else {
println!("Error status: {}", response.status());
}
// POST request
let client = reqwest::Client::new();
let response = client
.post("https://httpbin.org/post")
.body("raw body content")
.send()
.await?;
println!("POST response: {}", response.status());
// PUT request
let response = client
.put("https://httpbin.org/put")
.body("put body")
.send()
.await?;
println!("PUT status: {}", response.status());
// DELETE request
let response = client
.delete("https://httpbin.org/delete")
.send()
.await?;
println!("DELETE status: {}", response.status());
Ok(())
}The Client and Request Builder
use reqwest::{Client, ClientBuilder, Error};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Error> {
// Create a client with custom settings
let client = Client::builder()
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(5))
.user_agent("MyApp/1.0")
.build()?;
// Reuse the client for multiple requests
let resp1 = client.get("https://httpbin.org/get").send().await?;
let resp2 = client.get("https://httpbin.org/get").send().await?;
println!("Request 1: {}", resp1.status());
println!("Request 2: {}", resp2.status());
// Client with more options
let client = ClientBuilder::new()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.pool_max_idle_per_host(10) // Connection pool size
.pool_idle_timeout(Duration::from_secs(60))
.redirect(reqwest::redirect::Policy::limited(5)) // Follow up to 5 redirects
.gzip(true) // Enable gzip
.brotli(true) // Enable brotli
.build()?;
let response = client.get("https://httpbin.org/get").send().await?;
println!("Custom client: {}", response.status());
Ok(())
}
// Client with cookie support
#[cfg(feature = "cookies")]
fn cookie_client() -> Result<Client, Error> {
let client = Client::builder()
.cookie_store(true) // Enable cookie store
.build()?;
Ok(client)
}Query Parameters and Headers
use reqwest::{Client, header, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::new();
// Query parameters
let response = client
.get("https://httpbin.org/get")
.query(&[
("page", "1"),
("limit", "10"),
("sort", "desc"),
])
.send()
.await?;
println!("Query params: {}", response.status());
// Query with struct (uses serde URL encoding)
#[derive(serde::Serialize)]
struct Params {
page: u32,
limit: u32,
query: String,
}
let params = Params {
page: 1,
limit: 10,
query: "rust".to_string(),
};
let response = client
.get("https://httpbin.org/get")
.query(¶ms)
.send()
.await?;
println!("Struct params: {}", response.status());
// Single header
let response = client
.get("https://httpbin.org/headers")
.header("X-Custom-Header", "value")
.send()
.await?;
println!("Custom header: {}", response.status());
// Multiple headers
let response = client
.get("https://httpbin.org/headers")
.header("X-API-Key", "secret-key")
.header("X-Request-ID", "12345")
.header(header::ACCEPT, "application/json")
.header(header::USER_AGENT, "MyApp/1.0")
.send()
.await?;
println!("Multiple headers: {}", response.status());
// Header map
let mut headers = header::HeaderMap::new();
headers.insert("X-Auth-Token", "my-token".parse().unwrap());
headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
let response = client
.get("https://httpbin.org/headers")
.headers(headers)
.send()
.await?;
println!("Header map: {}", response.status());
Ok(())
}Request Bodies
use reqwest::{Client, header, Error};
use serde::{Serialize, Deserialize};
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::new();
// Plain text body
let response = client
.post("https://httpbin.org/post")
.header(header::CONTENT_TYPE, "text/plain")
.body("Plain text body")
.send()
.await?;
println!("Text body: {}", response.status());
// JSON body from string
let response = client
.post("https://httpbin.org/post")
.header(header::CONTENT_TYPE, "application/json")
.body(r#"{"name": "Alice", "age": 30}"#)
.send()
.await?;
println!("JSON string body: {}", response.status());
// JSON body with json() method (requires json feature)
#[derive(Serialize)]
struct User {
name: String,
email: String,
}
let user = User {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let response = client
.post("https://httpbin.org/post")
.json(&user)
.send()
.await?;
println!("JSON struct body: {}", response.status());
// Form data
let response = client
.post("https://httpbin.org/post")
.form(&[
("username", "alice"),
("password", "secret"),
])
.send()
.await?;
println!("Form body: {}", response.status());
// Form from struct
#[derive(Serialize)]
struct LoginForm {
username: String,
password: String,
}
let form = LoginForm {
username: "bob".to_string(),
password: "secret123".to_string(),
};
let response = client
.post("https://httpbin.org/post")
.form(&form)
.send()
.await?;
println!("Form struct body: {}", response.status());
// Binary body
let data = vec![0u8, 1, 2, 3, 4, 5];
let response = client
.post("https://httpbin.org/post")
.body(data)
.send()
.await?;
println!("Binary body: {}", response.status());
// Body from file (requires async)
// let file = tokio::fs::File::open("data.bin").await?;
// let response = client
// .post("https://httpbin.org/post")
// .body(file)
// .send()
// .await?;
Ok(())
}Response Handling
use reqwest::{Client, Error, Response, StatusCode};
use serde::Deserialize;
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::new();
// Get response object
let response = client.get("https://httpbin.org/get").send().await?;
// Status code
println!("Status: {}", response.status());
println!("Is success: {}", response.status().is_success());
println!("Is client error: {}", response.status().is_client_error());
println!("Is server error: {}", response.status().is_server_error());
// Headers
for (name, value) in response.headers() {
println!("{}: {:?}", name, value);
}
// Get specific header
if let Some(content_type) = response.headers().get("content-type") {
println!("Content-Type: {:?}", content_type);
}
// Response body as text
let text = response.text().await?;
println!("Body length: {} bytes", text.len());
// Response body as bytes
let response = client.get("https://httpbin.org/bytes/100").send().await?;
let bytes = response.bytes().await?;
println!("Bytes: {} bytes", bytes.len());
// JSON response
#[derive(Deserialize, Debug)]
struct HttpBinResponse {
url: String,
headers: std::collections::HashMap<String, String>,
}
let response = client.get("https://httpbin.org/get").send().await?;
let json: HttpBinResponse = response.json().await?;
println!("JSON response: {:?}", json);
// Check for error status
let response = client.get("https://httpbin.org/status/404").send().await?;
match response.error_for_status() {
Ok(resp) => println!("Success: {}", resp.status()),
Err(err) => println!("Error: {}", err),
}
// error_for_status_ref doesn't consume response
let response = client.get("https://httpbin.org/get").send().await?;
if let Err(err) = response.error_for_status_ref() {
println!("Request failed: {}", err);
} else {
let body = response.text().await?;
println!("Body: {}", body.chars().take(100).collect::<String>());
}
Ok(())
}
// Helper function for error handling
async fn fetch_json<T: for<'de> Deserialize<'de>>(url: &str) -> Result<T, Box<dyn std::error::Error>> {
let response = reqwest::get(url).await?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()).into());
}
let data: T = response.json().await?;
Ok(data)
}Timeouts and Error Handling
use reqwest::{Client, Error, StatusCode};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Error> {
// Client with timeouts
let client = Client::builder()
.timeout(Duration::from_secs(10)) // Total request timeout
.connect_timeout(Duration::from_secs(5)) // Connection timeout
.read_timeout(Duration::from_secs(5)) // Read timeout
.build()?;
// Request with timeout
let response = client
.get("https://httpbin.org/delay/2")
.timeout(Duration::from_secs(5))
.send()
.await?;
println!("Response: {}", response.status());
// Error handling
match fetch_with_error_handling("https://httpbin.org/status/500").await {
Ok(body) => println!("Success: {}", body),
Err(e) => println!("Error: {}", e),
}
Ok(())
}
async fn fetch_with_error_handling(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()?;
let response = client.get(url).send().await?;
// Check status code
match response.status() {
StatusCode::OK => {
let body = response.text().await?;
Ok(body)
}
StatusCode::NOT_FOUND => {
Err("Resource not found".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())
}
_ => {
let body = response.text().await?;
Ok(body)
}
}
}
// Comprehensive error handling
async fn robust_fetch(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?; // Network errors
// Convert HTTP errors to reqwest::Error
let response = response.error_for_status()?;
let body = response.text().await?; // Body read errors
Ok(body)
}
// With retry logic
async 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 retries = 0;
loop {
match client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
return Ok(response.text().await?);
} else if response.status().is_server_error() && retries < max_retries {
retries += 1;
tokio::time::sleep(Duration::from_millis(100 * retries as u64)).await;
continue;
} else {
return Err(format!("HTTP error: {}", response.status()).into());
}
}
Err(e) => {
if retries < max_retries {
retries += 1;
tokio::time::sleep(Duration::from_millis(100 * retries as u64)).await;
continue;
}
return Err(e.into());
}
}
}
}Authentication
use reqwest::{Client, header, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::new();
// Basic authentication
let response = client
.get("https://httpbin.org/basic-auth/user/pass")
.basic_auth("user", Some("pass"))
.send()
.await?;
println!("Basic auth: {}", response.status());
// Bearer token
let token = "my-secret-token";
let response = client
.get("https://httpbin.org/bearer")
.bearer_auth(token)
.send()
.await?;
println!("Bearer auth: {}", response.status());
// Custom Authorization header
let response = client
.get("https://httpbin.org/headers")
.header(header::AUTHORIZATION, "Token my-api-key")
.send()
.await?;
println!("Custom auth header: {}", response.status());
// API key in header
let response = client
.get("https://httpbin.org/headers")
.header("X-API-Key", "your-api-key")
.send()
.await?;
println!("API key header: {}", response.status());
Ok(())
}
// Authenticated client wrapper
struct ApiClient {
client: Client,
base_url: String,
}
impl ApiClient {
fn new(base_url: &str, token: &str) -> Result<Self, Error> {
let mut headers = header::HeaderMap::new();
let auth_value = format!("Bearer {}", token);
headers.insert(
header::AUTHORIZATION,
auth_value.parse().unwrap(),
);
let client = Client::builder()
.default_headers(headers)
.build()?;
Ok(Self {
client,
base_url: base_url.to_string(),
})
}
async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
let url = format!("{}{}", self.base_url, path);
self.client.get(&url).send().await?.json().await
}
async fn post<T: serde::Serialize, R: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &T,
) -> Result<R, Error> {
let url = format!("{}{}", self.base_url, path);
self.client
.post(&url)
.json(body)
.send()
.await?
.json()
.await
}
}Multipart Form Uploads
use reqwest::{Client, multipart, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::new();
// Simple multipart form
let form = multipart::Form::new()
.text("username", "alice")
.text("email", "alice@example.com");
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("Multipart form: {}", response.status());
// File upload
let file_content = "This is file content";
let part = multipart::Part::text(file_content)
.file_name("document.txt")
.mime_str("text/plain")?;
let form = multipart::Form::new()
.part("file", part)
.text("description", "My document");
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("File upload: {}", response.status());
// Multiple files
let form = multipart::Form::new()
.text("title", "Multiple files")
.part("file1", multipart::Part::text("Content 1")
.file_name("file1.txt"))
.part("file2", multipart::Part::text("Content 2")
.file_name("file2.txt"));
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("Multiple files: {}", response.status());
// Binary file upload
let binary_data = vec![0u8, 1, 2, 3, 4, 5];
let part = multipart::Part::bytes(binary_data)
.file_name("data.bin")
.mime_str("application/octet-stream")?;
let form = multipart::Form::new()
.part("binary", part);
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("Binary upload: {}", response.status());
Ok(())
}
// Upload file from disk
async fn upload_file(client: &Client, path: &str) -> Result<(), Error> {
let file = tokio::fs::read(path).await?;
let file_name = std::path::Path::new(path)
.file_name()
.unwrap()
.to_str()
.unwrap();
let part = multipart::Part::bytes(file)
.file_name(file_name.to_string());
let form = multipart::Form::new()
.part("file", part);
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("Uploaded {}", file_name);
Ok(())
}Streaming Responses
use reqwest::{Client, Error};
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::new();
// Stream response body as bytes
let response = client
.get("https://httpbin.org/stream-bytes/1000")
.send()
.await?;
let mut stream = response.bytes_stream();
let mut total = 0;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
total += chunk.len();
println!("Received {} bytes (total: {})", chunk.len(), total);
}
// Stream to file
let response = client
.get("https://httpbin.org/bytes/10000")
.send()
.await?;
let mut file = tokio::fs::File::create("downloaded.bin").await?;
let mut stream = response.bytes_stream();
use tokio::io::AsyncWriteExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
}
println!("Download complete");
// Clean up
tokio::fs::remove_file("downloaded.bin").await?;
Ok(())
}
// Download with progress
async fn download_with_progress(
url: &str,
output_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = client.get(url).send().await?;
let total_size = response.content_length().unwrap_or(0);
println!("Downloading {} bytes", total_size);
let mut file = tokio::fs::File::create(output_path).await?;
let mut stream = response.bytes_stream();
let mut downloaded = 0u64;
use tokio::io::AsyncWriteExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
downloaded += chunk.len() as u64;
if total_size > 0 {
let percent = (downloaded as f64 / total_size as f64) * 100.0;
print!("\rProgress: {:.1}%", percent);
}
}
println!("\nDownload complete!");
Ok(())
}Async Concurrent Requests
use reqwest::{Client, Error};
use futures::future;
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::new();
// Multiple concurrent requests
let urls = vec![
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/1",
];
// Create futures for all requests
let requests: Vec<_> = urls
.iter()
.map(|url| client.get(*url).send())
.collect();
// Execute all requests concurrently
let responses = future::join_all(requests).await;
for (i, result) in responses.into_iter().enumerate() {
match result {
Ok(resp) => println!("Request {}: {}", i, resp.status()),
Err(e) => println!("Request {} error: {}", i, e),
}
}
Ok(())
}
// Fetch multiple URLs with results
async fn fetch_all(urls: &[&str]) -> Vec<Result<String, reqwest::Error>> {
let client = Client::new();
let requests: Vec<_> = urls
.iter()
.map(|url| {
let client = &client;
async move {
let resp = client.get(*url).send().await?;
let text = resp.text().await?;
Ok::<_, reqwest::Error>(text)
}
})
.collect();
futures::future::join_all(requests).await
}
// Concurrent with limit
async fn fetch_with_limit(urls: &[&str], concurrency: usize) -> Vec<Result<String, reqwest::Error>> {
use futures::stream::{self, StreamExt};
let client = Client::new();
let results = stream::iter(urls)
.map(|url| {
let client = &client;
async move {
let resp = client.get(*url).send().await?;
let text = resp.text().await?;
Ok::<_, reqwest::Error>(text)
}
})
.buffer_unordered(concurrency)
.collect::<Vec<_>>()
.await;
results
}Blocking Client
# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }use reqwest::blocking::Client;
use reqwest::Error;
use serde::Deserialize;
fn main() -> Result<(), Error> {
// Simple blocking GET
let body = reqwest::blocking::get("https://httpbin.org/get")?.text()?;
println!("Response: {}", &body[..100.min(body.len())]);
// Blocking client
let client = Client::new();
// GET request
let response = client
.get("https://httpbin.org/get")
.query(&[("key", "value")])
.send()?;
println!("GET: {}", response.status());
// POST with JSON
#[derive(serde::Serialize, Deserialize)]
struct User {
name: String,
email: String,
}
let user = User {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let response = client
.post("https://httpbin.org/post")
.json(&user)
.send()?;
println!("POST: {}", response.status());
// JSON response
let response = client
.get("https://jsonplaceholder.typicode.com/users/1")
.send()?;
let user: User = response.json()?;
println!("User: {}", user.name);
Ok(())
}Real-World API Client
use reqwest::{Client, Error, StatusCode};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Serialize, Deserialize)]
struct Post {
id: u32,
title: String,
body: String,
userId: u32,
}
#[derive(Debug, Serialize)]
struct CreatePost {
title: String,
body: String,
userId: u32,
}
struct JsonPlaceholderApi {
client: Client,
base_url: String,
}
impl JsonPlaceholderApi {
fn new() -> Result<Self, Error> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
Ok(Self {
client,
base_url: "https://jsonplaceholder.typicode.com".to_string(),
})
}
async fn get_posts(&self) -> Result<Vec<Post>, Error> {
let url = format!("{}/posts", self.base_url);
self.client
.get(&url)
.send()
.await?
.json()
.await
}
async fn get_post(&self, id: u32) -> Result<Option<Post>, Error> {
let url = format!("{}/posts/{}", self.base_url, id);
let response = self.client
.get(&url)
.send()
.await?;
if response.status() == StatusCode::NOT_FOUND {
return Ok(None);
}
let post: Post = response.json().await?;
Ok(Some(post))
}
async fn create_post(&self, post: CreatePost) -> Result<Post, Error> {
let url = format!("{}/posts", self.base_url);
self.client
.post(&url)
.json(&post)
.send()
.await?
.json()
.await
}
async fn update_post(&self, id: u32, post: CreatePost) -> Result<Post, Error> {
let url = format!("{}/posts/{}", self.base_url, id);
self.client
.put(&url)
.json(&post)
.send()
.await?
.json()
.await
}
async fn delete_post(&self, id: u32) -> Result<bool, Error> {
let url = format!("{}/posts/{}", self.base_url, id);
let response = self.client
.delete(&url)
.send()
.await?;
Ok(response.status().is_success())
}
async fn get_posts_by_user(&self, user_id: u32) -> Result<Vec<Post>, Error> {
let url = format!("{}/posts", self.base_url);
self.client
.get(&url)
.query(&[("userId", user_id)])
.send()
.await?
.json()
.await
}
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let api = JsonPlaceholderApi::new()?;
// Get all posts
let posts = api.get_posts().await?;
println!("Found {} posts", posts.len());
// Get single post
if let Some(post) = api.get_post(1).await? {
println!("Post 1: {}", post.title);
}
// Create post
let new_post = CreatePost {
title: "My Post".to_string(),
body: "Post content".to_string(),
userId: 1,
};
let created = api.create_post(new_post).await?;
println!("Created post with ID: {}", created.id);
// Get posts by user
let user_posts = api.get_posts_by_user(1).await?;
println!("User 1 has {} posts", user_posts.len());
Ok(())
}Summary
- Use
reqwest::get(url).await?for simple GET requests - Use
Client::new()for reusable client with connection pooling - Use
.json(&data)for JSON request bodies (requiresjsonfeature) - Use
.form(&data)for form-encoded requests - Use
.query(¶ms)for query parameters - Use
.header(name, value)to add headers - Use
.timeout(Duration)for request timeouts - Use
.bearer_auth(token)or.basic_auth(user, pass)for authentication - Use
response.json::<T>().await?to deserialize JSON responses - Use
response.text().await?for text,response.bytes().await?for bytes - Use
response.error_for_status()?to convert HTTP errors to Result::Err - Use
multipart::Formfor file uploads - Use
response.bytes_stream()for streaming responses - Use
futures::future::join_allfor concurrent requests - Use
reqwest::blockingfor synchronous requests (requiresblockingfeature) - Configure client with
Client::builder()for timeouts, headers, cookies - HTTPS is enabled by default with native TLS
- Use
.multipart(form)for multipart/form-data uploads
