Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
reqwest::Client::execute handle automatic retries and redirect following?reqwest::Client::execute handles redirect following automatically by default, following up to 10 consecutive redirects and preserving the method and body for appropriate redirect types (301/302/303 with GET, 307/308 preserving the original method), while automatic retries must be explicitly configured through the client builder and require the retry feature flag. Redirects are enabled by default because they're ubiquitous in web APIs and browsersâwhen a server responds with 3xx status codes, the client automatically follows to the new location. Retries, however, are opt-in because retrying a request can have side effects: a POST that creates a resource shouldn't be automatically retried just because the network timed out. The ClientBuilder provides redirect policy configuration for redirect behavior and requires enabling the __internal_retry_after feature or using middleware like tower-http for retry functionality.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Build a request
let request = client
.get("https://httpbin.org/get")
.build()?;
// Execute the request
let response = client.execute(request).await?;
println!("Status: {}", response.status());
println!("Headers: {:?}", response.headers());
let body = response.text().await?;
println!("Body: {}", body);
Ok(())
}Client::execute sends the request and returns the response.
use reqwest::{Client, RedirectPolicy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Default client follows redirects automatically
let client = Client::new();
// This URL redirects
let response = client
.get("https://httpbin.org/redirect/2")
.send()
.await?;
// response is from the final URL after following redirects
println!("Final URL: {:?}", response.url());
println!("Status: {}", response.status());
Ok(())
}By default, reqwest follows up to 10 redirects.
use reqwest::{Client, RedirectPolicy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// No redirects
let no_redirects = Client::builder()
.redirect(RedirectPolicy::none())
.build()?;
// Limited redirects
let limited = Client::builder()
.redirect(RedirectPolicy::limited(5))
.build()?;
// Unlimited redirects (be careful of loops!)
let unlimited = Client::builder()
.redirect(RedirectPolicy::limited(100))
.build()?;
// Custom redirect policy
let custom = Client::builder()
.redirect(RedirectPolicy::custom(|attempt| {
// Stop after 3 redirects
if attempt.previous().len() >= 3 {
attempt.stop()
} else {
attempt.follow()
}
}))
.build()?;
Ok(())
}RedirectPolicy controls redirect behavior: none, limited, or custom.
use reqwest::{Client, RedirectPolicy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.redirect(RedirectPolicy::custom(|attempt| {
let url = attempt.url().clone();
println!("Redirect to: {}", url);
// Don't follow redirects to different hosts
if attempt.previous().len() > 0 {
let original_host = attempt.previous()[0].host_str();
let new_host = url.host_str();
if original_host != new_host {
println!("Refusing cross-host redirect");
return attempt.stop();
}
}
// Don't follow more than 5 redirects
if attempt.previous().len() >= 5 {
return attempt.stop();
}
attempt.follow()
}))
.build()?;
let response = client
.get("https://httpbin.org/redirect/3")
.send()
.await?;
println!("Final status: {}", response.status());
Ok(())
}Custom policies allow fine-grained control over redirect following.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = client
.get("https://httpbin.org/redirect/3")
.send()
.await?;
// Access the redirect chain
println!("Final URL: {}", response.url());
// The response contains the final URL
// To see the redirect chain, use redirect policy with tracking
// or check history manually
Ok(())
}The Response::url() gives the final URL after redirects.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// 301/302/303: Follow with GET (drop body)
// 307/308: Follow with same method and body
// POST with 302 redirect
let response = client
.post("https://httpbin.org/status/302")
.body("original body")
.send()
.await?;
// The redirect is followed, method becomes GET
println!("Status: {}", response.status());
// 307 preserves the method
let response = client
.post("https://httpbin.org/redirect-to?url=https://httpbin.org/post&status_code=307")
.body("preserved body")
.send()
.await?;
// Method remains POST, body preserved
println!("Status after 307: {}", response.status());
Ok(())
}Different redirect codes affect how the method and body are handled.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// For 301/302/303, the body is dropped and method becomes GET
let response = client
.post("https://httpbin.org/redirect-to?url=https://httpbin.org/get")
.json(&serde_json::json!({ "key": "value" }))
.send()
.await?;
println!("Final method: {:?}", response.remote_addr());
// For 307/308, the method and body are preserved
let response = client
.post("https://httpbin.org/redirect-to?url=https://httpbin.org/post&status_code=307")
.json(&serde_json::json!({ "key": "value" }))
.send()
.await?;
println!("307 redirect status: {}", response.status());
Ok(())
}301/302/303 convert to GET; 307/308 preserve the original method.
use reqwest::Client;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// This request will fail once, and reqwest will NOT retry
// The error is returned immediately
let result = client
.get("https://httpbin.org/delay/10") // Takes 10 seconds
.timeout(Duration::from_secs(1))
.send()
.await;
match result {
Ok(response) => println!("Success: {}", response.status()),
Err(e) => println!("Error (not retried): {}", e),
}
Ok(())
}By default, reqwest does NOT retry failed requests.
use reqwest::Client;
use reqwest_retry::{RetryTransientMiddleware, RetryPolicy};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Use reqwest-retry crate for retry functionality
let retry_policy = RetryPolicy::builder()
.retry(3)
.build();
let client = reqwest_middleware::ClientBuilder::new(Client::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
let response = client
.get("https://httpbin.org/get")
.send()
.await?;
println!("Status: {}", response.status());
Ok(())
}Retry functionality requires external crates like reqwest-retry or tower-http.
use reqwest::Client;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let max_retries = 3;
let url = "https://httpbin.org/get";
let mut attempts = 0;
let response = loop {
attempts += 1;
match client.get(url).send().await {
Ok(response) => break response,
Err(e) => {
if attempts >= max_retries {
return Err(e.into());
}
// Check if error is retryable
if e.is_timeout() || e.is_connect() {
println!("Attempt {} failed: {}, retrying...", attempts, e);
tokio::time::sleep(Duration::from_millis(100 * attempts as u64)).await;
continue;
}
// Non-retryable error
return Err(e.into());
}
}
};
println!("Success after {} attempts", attempts);
Ok(())
}Manual retry logic gives full control over retry conditions.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Retryable errors:
// - Timeout (is_timeout())
// - Connection errors (is_connect())
// - 5xx status codes (server errors)
// Non-retryable errors:
// - 4xx status codes (client errors)
// - Invalid URL
// - Invalid headers
let result = client
.get("https://httpbin.org/status/500") // Server error
.send()
.await?;
// 500 is a valid response, not an error
// To treat 5xx as errors, check status
if result.status().is_server_error() {
println!("Server error (could retry)");
}
Ok(())
}Distinguish between transport errors (retryable) and response errors (may not be retryable).
use reqwest::Client;
use std::time::Duration;
async fn fetch_with_retry(
client: &Client,
url: &str,
max_retries: u32,
) -> Result<reqwest::Response, reqwest::Error> {
let mut attempts = 0;
loop {
match client.get(url).send().await {
Ok(response) => {
// Check status code
if response.status().is_server_error() && attempts < max_retries {
attempts += 1;
let delay = Duration::from_millis(100 * 2u64.pow(attempts));
tokio::time::sleep(delay).await;
continue;
}
return Ok(response);
}
Err(e) => {
attempts += 1;
if attempts >= max_retries || !is_retryable(&e) {
return Err(e);
}
let delay = Duration::from_millis(100 * 2u64.pow(attempts));
println!("Attempt {} failed, retrying in {:?}", attempts, delay);
tokio::time::sleep(delay).await;
}
}
}
}
fn is_retryable(error: &reqwest::Error) -> bool {
error.is_timeout() || error.is_connect() || error.is_request()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = fetch_with_retry(&client, "https://httpbin.org/get", 3).await?;
println!("Status: {}", response.status());
Ok(())
}Exponential backoff prevents overwhelming a struggling server.
use reqwest::{Client, header};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Authorization header is NOT sent on cross-origin redirects
let response = client
.get("https://httpbin.org/bearer")
.header(header::AUTHORIZATION, "Bearer secret-token")
.send()
.await?;
// reqwest automatically strips sensitive headers on cross-host redirects
println!("Status: {}", response.status());
Ok(())
}Sensitive headers like Authorization are stripped on cross-origin redirects.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Relative redirects are resolved against the original URL
let response = client
.get("https://httpbin.org/relative-redirect/2")
.send()
.await?;
println!("Final URL: {}", response.url());
println!("Status: {}", response.status());
Ok(())
}Relative redirect URLs are resolved against the previous URL.
use reqwest::{Client, RedirectPolicy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Default policy limits to 10 redirects
// This prevents infinite loops
let client = Client::new();
// If server returns infinite redirects, client stops after 10
let result = client
.get("https://httpbin.org/redirect/15") // Would need 15 redirects
.send()
.await;
match result {
Ok(_) => println!("Success (unlikely)"),
Err(e) => println!("Error (too many redirects): {}", e),
}
// With custom limit
let client = Client::builder()
.redirect(RedirectPolicy::limited(5))
.build()?;
let result = client
.get("https://httpbin.org/redirect/10")
.send()
.await;
match result {
Ok(_) => println!("Success"),
Err(e) => println!("Too many redirects: {}", e),
}
Ok(())
}RedirectPolicy::limited prevents infinite redirect loops.
use reqwest::{Client, RedirectPolicy};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.redirect(RedirectPolicy::limited(10))
.timeout(Duration::from_secs(30))
.build()?;
// Function that handles both redirects and retries
async fn fetch(client: &Client, url: &str, max_retries: u32) -> Result<reqwest::Response, reqwest::Error> {
let mut attempts = 0;
loop {
match client.get(url).send().await {
Ok(response) => return Ok(response),
Err(e) => {
attempts += 1;
if attempts >= max_retries {
return Err(e);
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
}
let response = fetch(&client, "https://httpbin.org/get", 3).await?;
println!("Status: {}", response.status());
Ok(())
}Redirects are handled automatically; retries must be implemented manually or via middleware.
use reqwest::{Client, RedirectPolicy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Disable redirects to see the redirect response
let client = Client::builder()
.redirect(RedirectPolicy::none())
.build()?;
let response = client
.get("https://httpbin.org/redirect/1")
.send()
.await?;
// See the actual 302 response
println!("Status: {}", response.status());
if response.status().is_redirection() {
println!("Location: {:?}", response.headers().get("location"));
// Manually follow if needed
if let Some(location) = response.headers().get("location") {
let new_url = location.to_str()?;
println!("Would redirect to: {}", new_url);
}
}
Ok(())
}Disabling redirects lets you inspect redirect responses directly.
use reqwest::{Client, RedirectPolicy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// POST with body to a redirecting URL
let response = client
.post("https://httpbin.org/redirect-to?url=https://httpbin.org/post")
.json(&serde_json::json!({ "data": "test" }))
.send()
.await?;
// For 301/302/303, method becomes GET and body is lost
println!("Final status: {}", response.status());
// For 307/308, preserve method and body
let response = client
.post("https://httpbin.org/redirect-to?url=https://httpbin.org/post&status_code=307")
.json(&serde_json::json!({ "data": "preserved" }))
.send()
.await?;
println!("307 status: {}", response.status());
Ok(())
}Be aware of how redirects affect request bodies.
Redirect behavior by status code:
| Status Code | Method Change | Body Handling | |-------------|---------------|---------------| | 301 (Moved) | GET | Dropped | | 302 (Found) | GET | Dropped | | 303 (See Other) | GET | Dropped | | 307 (Temp Redirect) | Preserved | Preserved | | 308 (Perm Redirect) | Preserved | Preserved |
Built-in vs. requires middleware:
| Feature | Built-in | Requires Setup |
|---------|----------|----------------|
| Redirect following | Yes (default) | RedirectPolicy::none() to disable |
| Redirect limits | Yes (10) | RedirectPolicy::limited(n) |
| Retry on error | No | Requires reqwest-retry or manual |
| Exponential backoff | No | Requires implementation |
| Status-based retry | No | Requires implementation |
When redirects happen automatically:
| Scenario | Behavior | |----------|----------| | Response is 3xx | Follow Location header | | Cross-host redirect | Strip sensitive headers | | Relative Location | Resolve against previous URL | | Too many redirects | Return error |
Key insight: reqwest::Client::execute handles redirect following as a built-in, configurable feature because redirects are a standard HTTP mechanism where the server explicitly tells the client to look elsewhereâthere's no ambiguity about whether to follow, only how many hops to allow. Retries, however, are opt-in because they require application-specific logic: should a POST be retried after a timeout? What about a 503 Service Unavailable? The answer depends on whether the operation is idempotent, whether the server acknowledged receipt, and what consistency guarantees the application needs. The redirect policy lets you say "stop after N redirects" or "don't follow cross-host redirects," but retry logic requires you to decide which errors are transient, how many times to retry, and how long to wait between attempts. The reqwest-retry crate provides sensible defaults (retry on 5xx and network errors, use exponential backoff), but the ultimate control lives in your codeâbecause only you know whether retrying a request is safe for your application's data integrity.