Debugging a Reqwest client effectively requires a multi-layered approach. This guide covers the most practical debugging strategies, from basic logging to advanced network inspection techniques.
Quick Start: Basic Debug Output
For immediate debugging, use Rust's dbg!
macro or println!
to inspect request/response data:
use reqwest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let response = reqwest::get("https://httpbin.org/get").await?;
// Quick debug output
dbg!(&response.status());
dbg!(&response.headers());
// Print response body
let text = response.text().await?;
println!("Response: {}", text);
Ok(())
}
1. Structured Logging with tracing
Modern Rust applications should use the tracing
crate for better observability:
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tracing = "0.1"
tracing-subscriber = "0.3"
tokio = { version = "1.0", features = ["full"] }
use tracing::{info, warn, error, instrument};
use tracing_subscriber;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing subscriber
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
debug_request().await?;
Ok(())
}
#[instrument]
async fn debug_request() -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
info!("Starting HTTP request");
let response = client
.get("https://httpbin.org/get")
.header("User-Agent", "debug-client/1.0")
.send()
.await?;
info!("Response status: {}", response.status());
info!("Response headers: {:?}", response.headers());
let body = response.text().await?;
info!("Response body length: {} bytes", body.len());
Ok(())
}
2. Enable Reqwest Internal Logging
Reqwest provides detailed internal logging when enabled:
# Show all reqwest debug information
RUST_LOG=reqwest=debug cargo run
# Show trace-level information (very verbose)
RUST_LOG=reqwest=trace cargo run
# Filter specific modules
RUST_LOG=reqwest::async_impl=debug,hyper=info cargo run
3. Comprehensive Error Handling
Implement detailed error analysis to catch different failure modes:
use reqwest::{Error, StatusCode};
async fn debug_with_error_handling(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
match client.get(url).send().await {
Ok(response) => {
let status = response.status();
let headers = response.headers().clone();
println!("✓ Request successful");
println!(" Status: {}", status);
println!(" Headers: {:#?}", headers);
match status {
StatusCode::OK => {
let content_type = headers.get("content-type")
.and_then(|ct| ct.to_str().ok())
.unwrap_or("unknown");
println!(" Content-Type: {}", content_type);
let body = response.text().await?;
println!(" Body length: {} bytes", body.len());
Ok(body)
}
StatusCode::NOT_FOUND => {
eprintln!("✗ Resource not found (404)");
Err("Resource not found".into())
}
StatusCode::UNAUTHORIZED => {
eprintln!("✗ Authentication required (401)");
Err("Authentication failed".into())
}
StatusCode::TOO_MANY_REQUESTS => {
eprintln!("✗ Rate limited (429)");
if let Some(retry_after) = headers.get("retry-after") {
eprintln!(" Retry after: {:?}", retry_after);
}
Err("Rate limited".into())
}
_ => {
eprintln!("✗ HTTP error: {}", status);
let error_body = response.text().await.unwrap_or_default();
eprintln!(" Error details: {}", error_body);
Err(format!("HTTP {}", status).into())
}
}
}
Err(error) => {
eprintln!("✗ Request failed: {}", error);
// Analyze different error types
if error.is_timeout() {
eprintln!(" → Timeout occurred");
} else if error.is_connect() {
eprintln!(" → Connection failed");
} else if error.is_decode() {
eprintln!(" → Response decoding failed");
} else if error.is_redirect() {
eprintln!(" → Redirect loop detected");
}
// Chain error for more context
if let Some(source) = error.source() {
eprintln!(" → Root cause: {}", source);
}
Err(error.into())
}
}
}
4. Request Inspection Before Sending
Debug the request construction before it's sent:
async fn inspect_request() -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
let request = client
.post("https://httpbin.org/post")
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"name": "test",
"value": 42
}))
.build()?;
// Inspect the built request
println!("Request URL: {}", request.url());
println!("Request method: {}", request.method());
println!("Request headers:");
for (name, value) in request.headers() {
println!(" {}: {:?}", name, value);
}
// Check if request has a body
if let Some(body) = request.body() {
println!("Request has body: {} bytes", body.as_bytes().map_or(0, |b| b.len()));
}
// Execute the request
let response = client.execute(request).await?;
println!("Response status: {}", response.status());
Ok(())
}
5. Network Traffic Analysis
Using mitmproxy for HTTP Debugging
Set up a debugging proxy to inspect all traffic:
use reqwest::Proxy;
async fn debug_with_proxy() -> Result<(), reqwest::Error> {
let client = reqwest::Client::builder()
.proxy(Proxy::http("http://127.0.0.1:8080")?) // mitmproxy default
.danger_accept_invalid_certs(true) // For HTTPS debugging
.build()?;
let response = client
.get("https://httpbin.org/get")
.send()
.await?;
println!("Status: {}", response.status());
Ok(())
}
Start mitmproxy in another terminal:
# Install mitmproxy
pip install mitmproxy
# Start proxy server
mitmproxy -p 8080
Using reqwest-middleware for Request/Response Logging
For production-ready logging, use the reqwest-middleware
crate:
[dependencies]
reqwest-middleware = "0.2"
reqwest-tracing = "0.4"
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_tracing::TracingMiddleware;
async fn with_middleware() -> Result<(), Box<dyn std::error::Error>> {
let client: ClientWithMiddleware = ClientBuilder::new(reqwest::Client::new())
.with(TracingMiddleware::default())
.build();
let response = client
.get("https://httpbin.org/get")
.send()
.await?;
println!("Status: {}", response.status());
Ok(())
}
6. Performance Debugging
Monitor timing and performance metrics:
use std::time::Instant;
async fn debug_performance() -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
let start = Instant::now();
let response = client
.get("https://httpbin.org/delay/2")
.send()
.await?;
let duration = start.elapsed();
println!("Request took: {:?}", duration);
println!("Status: {}", response.status());
// Check response timing headers
if let Some(server_timing) = response.headers().get("server-timing") {
println!("Server timing: {:?}", server_timing);
}
Ok(())
}
7. Testing with Mock Servers
Use wiremock
for controlled testing scenarios:
[dev-dependencies]
wiremock = "0.5"
#[cfg(test)]
mod tests {
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};
#[tokio::test]
async fn test_debug_scenario() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test"))
.respond_with(ResponseTemplate::new(404))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/test", &mock_server.uri()))
.send()
.await
.unwrap();
assert_eq!(response.status(), 404);
}
}
Common Debugging Patterns
Debug Configuration Builder
fn create_debug_client() -> reqwest::Client {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.user_agent("debug-client/1.0")
.connection_verbose(true) // Enable verbose connection logs
.build()
.expect("Failed to create client")
}
Environment-based Debug Levels
fn setup_logging() {
let log_level = std::env::var("DEBUG_LEVEL")
.unwrap_or_else(|_| "info".to_string());
std::env::set_var("RUST_LOG", format!("reqwest={},my_app=debug", log_level));
env_logger::init();
}
These debugging strategies will help you identify and resolve issues quickly, from basic connectivity problems to complex authentication and performance issues. Start with basic logging and progressively add more sophisticated debugging tools as needed.