Error handling is crucial when building robust HTTP clients with Reqwest in Rust. Proper error handling ensures your application can gracefully handle network failures, server errors, and unexpected responses. This guide covers comprehensive best practices for handling errors effectively.
Understanding Reqwest Error Types
Reqwest provides several error types that you need to handle:
Primary Error Types
use reqwest::Error;
use std::error::Error as StdError;
// Main Reqwest error type
let error: reqwest::Error = /* ... */;
// Check specific error conditions
if error.is_timeout() {
println!("Request timed out");
} else if error.is_connect() {
println!("Connection failed");
} else if error.is_decode() {
println!("Response decoding failed");
} else if error.is_request() {
println!("Request construction failed");
}
Error Categories
- Network Errors: Connection failures, DNS resolution issues
- Timeout Errors: Request or connect timeouts
- HTTP Protocol Errors: Invalid responses, protocol violations
- Decode Errors: JSON/text parsing failures
- Request Building Errors: Invalid URLs, malformed headers
Async Error Handling Patterns
Basic Async Error Handling
use reqwest::Client;
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let client = Client::new();
let response = client.get(url).send().await?;
// Check status before processing
let response = response.error_for_status()?;
let text = response.text().await?;
Ok(text)
}
// Usage
#[tokio::main]
async fn main() {
match fetch_data("https://api.example.com/data").await {
Ok(data) => println!("Success: {}", data),
Err(e) => eprintln!("Error: {}", e),
}
}
Comprehensive Status Code Handling
use reqwest::{Client, StatusCode};
async fn handle_response(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = Client::new();
let response = client.get(url).send().await?;
match response.status() {
StatusCode::OK => {
let body = response.text().await?;
Ok(body)
},
StatusCode::NOT_FOUND => {
Err("Resource not found".into())
},
StatusCode::UNAUTHORIZED => {
Err("Authentication required".into())
},
StatusCode::TOO_MANY_REQUESTS => {
Err("Rate limit exceeded".into())
},
status if status.is_server_error() => {
Err(format!("Server error: {}", status).into())
},
status if status.is_client_error() => {
Err(format!("Client error: {}", status).into())
},
_ => {
Err(format!("Unexpected status: {}", response.status()).into())
}
}
}
Retry Logic and Circuit Breaker Pattern
Exponential Backoff Retry
use std::time::Duration;
use tokio::time::sleep;
async fn fetch_with_retry(
url: &str,
max_retries: u32,
) -> Result<String, reqwest::Error> {
let client = Client::new();
let mut delay = Duration::from_millis(100);
for attempt in 0..=max_retries {
match client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
return response.text().await;
} else if response.status().is_server_error() && attempt < max_retries {
// Retry on server errors
sleep(delay).await;
delay *= 2; // Exponential backoff
continue;
} else {
return Err(reqwest::Error::from(response.error_for_status().unwrap_err()));
}
},
Err(e) if e.is_timeout() && attempt < max_retries => {
sleep(delay).await;
delay *= 2;
continue;
},
Err(e) => return Err(e),
}
}
Err(reqwest::Error::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Max retries exceeded"
)))
}
Custom Error Types
Using thiserror for Better Error Handling
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApiError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Authentication failed")]
Authentication,
#[error("Rate limit exceeded, retry after {retry_after}s")]
RateLimit { retry_after: u64 },
#[error("Resource not found: {resource}")]
NotFound { resource: String },
#[error("Server error: {status}")]
ServerError { status: u16 },
#[error("Validation error: {message}")]
Validation { message: String },
}
async fn api_request(url: &str) -> Result<serde_json::Value, ApiError> {
let client = Client::new();
let response = client.get(url).send().await?;
match response.status() {
StatusCode::OK => {
let json = response.json().await?;
Ok(json)
},
StatusCode::UNAUTHORIZED => Err(ApiError::Authentication),
StatusCode::NOT_FOUND => Err(ApiError::NotFound {
resource: url.to_string(),
}),
StatusCode::TOO_MANY_REQUESTS => {
let retry_after = response
.headers()
.get("retry-after")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(60);
Err(ApiError::RateLimit { retry_after })
},
status if status.is_server_error() => {
Err(ApiError::ServerError { status: status.as_u16() })
},
_ => Err(ApiError::Network(response.error_for_status().unwrap_err())),
}
}
Timeout Configuration and Handling
Comprehensive Timeout Setup
use reqwest::Client;
use std::time::Duration;
fn create_robust_client() -> Result<Client, reqwest::Error> {
Client::builder()
.timeout(Duration::from_secs(30)) // Total request timeout
.connect_timeout(Duration::from_secs(10)) // Connection timeout
.pool_idle_timeout(Duration::from_secs(90)) // Connection pool timeout
.pool_max_idle_per_host(10) // Max idle connections
.user_agent("MyApp/1.0")
.build()
}
async fn fetch_with_timeouts(url: &str) -> Result<String, reqwest::Error> {
let client = create_robust_client()?;
let response = client
.get(url)
.timeout(Duration::from_secs(15)) // Per-request timeout
.send()
.await?;
response.error_for_status()?.text().await
}
Logging and Monitoring
Structured Error Logging
use log::{error, warn, info};
use serde_json::json;
async fn fetch_with_logging(url: &str) -> Result<String, reqwest::Error> {
let client = Client::new();
info!("Making request to: {}", url);
match client.get(url).send().await {
Ok(response) => {
let status = response.status();
let headers = response.headers().clone();
if status.is_success() {
info!("Request successful: {} {}", status, url);
response.text().await
} else {
warn!("HTTP error: {} {} - {}", status, url, status.canonical_reason().unwrap_or("Unknown"));
Err(response.error_for_status().unwrap_err())
}
},
Err(e) => {
let error_info = json!({
"url": url,
"error_type": if e.is_timeout() { "timeout" }
else if e.is_connect() { "connection" }
else if e.is_decode() { "decode" }
else { "other" },
"error_message": e.to_string()
});
error!("Request failed: {}", error_info);
Err(e)
}
}
}
Best Practices Summary
1. Always Handle Status Codes
// Use error_for_status() for automatic 4xx/5xx handling
let response = client.get(url).send().await?.error_for_status()?;
2. Implement Proper Timeouts
// Set both connection and total request timeouts
let client = Client::builder()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.build()?;
3. Use Result Type Propagation
// Use ? operator for clean error propagation
async fn api_call() -> Result<Data, MyError> {
let response = client.get(url).send().await?;
let data: Data = response.json().await?;
Ok(data)
}
4. Implement Retry Logic for Transient Errors
// Retry on specific error conditions
if error.is_timeout() || response.status().is_server_error() {
// Implement retry with backoff
}
5. Create Context-Specific Error Types
// Use custom error types for better error handling
#[derive(Error, Debug)]
enum MyApiError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
// ... other variants
}
By following these practices, you'll build robust HTTP clients that can handle various failure scenarios gracefully, provide meaningful error messages, and maintain good performance even under adverse network conditions.