How does reqwest::redirect::Policy::custom differ from limited for fine-grained redirect handling?
Policy::limited provides simple count-based redirect limiting while Policy::custom enables fine-grained control over each redirect decision by inspecting the request, response, and redirect target. The limited policy follows up to a specified number of redirects automatically—it's simple and covers most use cases. The custom policy accepts a closure that receives each redirect's details and returns an action: follow, stop, or error. This allows complex policies like domain restrictions, authentication handling, and logging during redirect chains.
Understanding Redirect Policies
use reqwest::redirect::Policy;
fn main() {
// limited(n) - follow at most n redirects
let limited_policy = Policy::limited(10);
// unlimited - follow all redirects (dangerous!)
let unlimited_policy = Policy::unlimited();
// none - don't follow any redirects
let no_redirects = Policy::none();
// custom - fine-grained control
let custom_policy = Policy::custom(|redirect| {
// Called for each redirect, can inspect and decide
// Returns redirect.action() or an error
redirect.follow()
});
println!("Policies created successfully");
}limited is count-based; custom gives per-redirect control.
The Limited Policy
use reqwest::redirect::Policy;
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with limited redirects
let client = Client::builder()
.redirect(Policy::limited(5))
.build()?;
// This will follow up to 5 redirects, then return an error
let response = client
.get("https://httpbin.org/redirect/3")
.send()
.await?;
println!("Final URL: {:?}", response.url());
println!("Status: {}", response.status());
Ok(())
}limited(5) follows at most 5 redirects before returning an error.
Custom Policy Basics
use reqwest::redirect::Policy;
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Custom policy receives redirect information
let client = Client::builder()
.redirect(Policy::custom(|redirect| {
println!("Redirect from {} to {}",
redirect.previous_url(),
redirect.url()
);
// Return one of:
// - redirect.follow() - follow the redirect
// - redirect.stop() - stop, return current response
// - Err(error) - return an error
redirect.follow()
}))
.build()?;
let response = client
.get("https://httpbin.org/redirect/2")
.send()
.await?;
println!("Final URL: {:?}", response.url());
Ok(())
}custom receives a Redirect struct with information about each redirect.
Domain Restriction with Custom Policy
use reqwest::redirect::Policy;
use reqreqwest::Client;
use url::Url;
fn is_safe_redirect(from: &Url, to: &Url) -> bool {
// Only allow redirects within the same domain
from.host() == to.host() && from.port() == to.port()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.redirect(Policy::custom(|redirect| {
let previous = redirect.previous_url();
let target = redirect.url();
if is_safe_redirect(previous, target) {
println!("Following safe redirect: {}", target);
redirect.follow()
} else {
// Stop on cross-domain redirect
println!("Stopping: cross-domain redirect from {} to {}",
previous, target
);
redirect.stop()
}
}))
.build()?;
let response = client
.get("https://example.com/api/endpoint")
.send()
.await?;
println!("Response from: {:?}", response.url());
Ok(())
}custom can restrict redirects to safe domains, preventing open redirect vulnerabilities.
Redirect Count Tracking
use reqwest::redirect::Policy;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Track redirect count across all requests
let redirect_count = Arc::new(AtomicUsize::new(0));
let redirect_count_clone = redirect_count.clone();
let client = reqwest::Client::builder()
.redirect(Policy::custom(move |redirect| {
// Increment counter
redirect_count_clone.fetch_add(1, Ordering::SeqCst);
// Limit to 10 total
if redirect_count_clone.load(Ordering::SeqCst) > 10 {
redirect.stop()
} else {
redirect.follow()
}
}))
.build()?;
let response = client
.get("https://httpbin.org/redirect/3")
.send()
.await?;
println!("Total redirects: {}", redirect_count.load(Ordering::SeqCst));
println!("Final URL: {:?}", response.url());
Ok(())
}custom enables tracking redirect behavior across requests.
Status Code-Based Decisions
use reqwest::redirect::Policy;
use http::StatusCode;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|redirect| {
// Access the redirect status code
let status = redirect.status();
match status {
// Always follow 301 and 302 (permanent/temporary)
StatusCode::MOVED_PERMANENTLY |
StatusCode::FOUND => redirect.follow(),
// For 307/308 (preserve method), be more careful
StatusCode::TEMPORARY_REDIRECT |
StatusCode::PERMANENT_REDIRECT => {
// These preserve the request method and body
// Only follow if it's safe
println!("Method-preserving redirect: {}", status);
redirect.follow()
}
// Unknown redirect status
_ => {
println!("Unexpected redirect status: {}", status);
redirect.stop()
}
}
}))
.build()?;
let response = client
.get("https://httpbin.org/redirect/1")
.send()
.await?;
println!("Response status: {}", response.status());
Ok(())
}custom allows status code-specific handling decisions.
Comparing limited and custom
use reqwest::redirect::Policy;
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// limited(5): Simple count-based limit
// - Follows up to 5 redirects
// - No inspection of redirect targets
// - No logging or custom logic
// - Returns error on limit exceeded
let limited_client = Client::builder()
.redirect(Policy::limited(5))
.build()?;
// custom: Full control
// - Inspect each redirect
// - Apply custom rules
// - Log or track redirects
// - Return errors for specific conditions
let custom_client = Client::builder()
.redirect(Policy::custom(|redirect| {
let from = redirect.previous_url();
let to = redirect.url();
// Block redirects to certain domains
if let Some(host) = to.host_str() {
if host.contains("malicious") || host.contains("tracking") {
return Err(reqwest::Error::new(
reqwest::error::Kind::Redirect,
Some(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Blocked redirect to: {}", host)
)))
));
}
}
redirect.follow()
}))
.build()?;
println!("Both clients configured");
Ok(())
}limited is simple count-based; custom enables security policies and logging.
Authentication During Redirects
use reqwest::redirect::Policy;
use reqwest::Client;
use url::Url;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Custom policy that handles authentication redirects
let client = Client::builder()
.redirect(Policy::custom(|redirect| {
let target = redirect.url();
// Check if redirect requires authentication
if target.username() != "" {
println!("Redirect requires auth: {}", target);
// Could prompt for credentials or use stored auth
}
// Handle internal vs external redirects differently
let previous = redirect.previous_url();
if previous.host() != target.host() {
println!("Cross-host redirect: {} -> {}",
previous.host_str().unwrap_or("?"),
target.host_str().unwrap_or("?")
);
// Could stop on cross-host redirects for security
}
redirect.follow()
}))
.build()?;
let response = client
.get("https://httpbin.org/redirect/1")
.send()
.await?;
println!("Final URL: {:?}", response.url());
Ok(())
}custom enables authentication handling and cross-host detection.
Logging Redirect Chains
use reqwest::redirect::Policy;
use reqwest::Client;
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Debug, Clone)]
struct RedirectLog {
from: String,
to: String,
status: u16,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let redirect_chain: Arc<Mutex<Vec<RedirectLog>>> = Arc::new(Mutex::new(Vec::new()));
let chain_clone = redirect_chain.clone();
let client = Client::builder()
.redirect(Policy::custom(move |redirect| {
// Log each redirect
let log_entry = RedirectLog {
from: redirect.previous_url().to_string(),
to: redirect.url().to_string(),
status: redirect.status().as_u16(),
};
if let Ok(mut chain) = chain_clone.lock() {
chain.push(log_entry);
}
redirect.follow()
}))
.build()?;
let response = client
.get("https://httpbin.org/redirect/3")
.send()
.await?;
// Print the redirect chain
if let Ok(chain) = redirect_chain.lock() {
println!("Redirect chain ({} hops):", chain.len());
for (i, entry) in chain.iter().enumerate() {
println!(" {}: {} -> {} (status: {})",
i, entry.from, entry.to, entry.status
);
}
}
println!("Final URL: {:?}", response.url());
Ok(())
}custom enables complete redirect chain logging for debugging.
Per-Host Redirect Limits
use reqwest::redirect::Policy;
use reqwest::Client;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Track redirect count per host
let per_host_count: Arc<Mutex<HashMap<String, usize>>> =
Arc::new(Mutex::new(HashMap::new()));
let count_clone = per_host_count.clone();
let client = Client::builder()
.redirect(Policy::custom(move |redirect| {
let target_host = redirect.url().host_str().unwrap_or("").to_string();
// Check/increment count for this host
let should_follow = {
if let Ok(mut counts) = count_clone.lock() {
let count = counts.entry(target_host.clone()).or_insert(0);
if *count >= 3 {
false // Don't follow more than 3 redirects per host
} else {
*count += 1;
true
}
} else {
false
}
};
if should_follow {
redirect.follow()
} else {
println!("Per-host redirect limit reached for: {}", target_host);
redirect.stop()
}
}))
.build()?;
println!("Client configured with per-host redirect limits");
Ok(())
}custom can implement per-host redirect counting, not possible with limited.
Handling Redirect Loops
use reqwest::redirect::Policy;
use reqwest::Client;
use std::collections::HashSet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Detect redirect loops by tracking visited URLs
let visited: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
let visited_clone = visited.clone();
let client = Client::builder()
.redirect(Policy::custom(move |redirect| {
let target = redirect.url().to_string();
// Check if we've seen this URL before
if let Ok(mut visited_set) = visited_clone.lock() {
if visited_set.contains(&target) {
println!("Redirect loop detected: {}", target);
return redirect.stop();
}
visited_set.insert(target);
}
redirect.follow()
}))
.build()?;
println!("Client configured with loop detection");
Ok(())
}custom enables loop detection through URL tracking.
Redirect Actions
use reqwest::redirect::Policy;
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.redirect(Policy::custom(|redirect| {
// Three possible actions:
// 1. follow() - continue with redirect
// redirect.follow()
// 2. stop() - stop redirecting, return current response
// redirect.stop()
// 3. Err() - return an error
// Err(reqwest::Error::new(...))
// Example: stop on certain conditions
if redirect.status().as_u16() == 301 {
// 301 = Moved Permanently
// Might want to cache this URL change
println!("Permanent redirect to: {}", redirect.url());
}
redirect.follow()
}))
.build()?;
println!("Client configured");
Ok(())
}redirect provides follow(), stop(), or can return Err for each redirect.
Security Considerations
use reqwest::redirect::Policy;
use reqwest::Client;
use url::Url;
fn is_private_ip(url: &Url) -> bool {
// Check if URL points to private/internal IP
if let Some(host) = url.host_str() {
// Simplified check - real implementation would be more thorough
host.starts_with("192.168.") ||
host.starts_with("10.") ||
host.starts_with("172.") ||
host == "localhost" ||
host.starts_with("127.")
} else {
false
}
}
fn is_https(url: &Url) -> bool {
url.scheme() == "https"
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.redirect(Policy::custom(|redirect| {
let previous = redirect.previous_url();
let target = redirect.url();
// Security: Block redirects to private IPs
if is_private_ip(target) {
println!("Blocked redirect to private IP: {}", target);
return redirect.stop();
}
// Security: Don't downgrade from HTTPS to HTTP
if is_https(previous) && !is_https(target) {
println!("Blocked HTTPS -> HTTP downgrade: {} -> {}",
previous, target
);
return redirect.stop();
}
redirect.follow()
}))
.build()?;
println!("Client configured with security policies");
Ok(())
}custom enables security policies against SSRF and downgrade attacks.
Synthesis
Quick reference:
use reqwest::redirect::Policy;
use reqwest::Client;
// limited(n): Simple count-based limit
let client = Client::builder()
.redirect(Policy::limited(10)) // Max 10 redirects
.build()?;
// custom: Fine-grained control
let client = Client::builder()
.redirect(Policy::custom(|redirect| {
// Access redirect information:
// - redirect.previous_url() - URL we're redirecting from
// - redirect.url() - URL we're redirecting to
// - redirect.status() - HTTP status code (301, 302, etc.)
// Return one of:
// - redirect.follow() - continue with redirect
// - redirect.stop() - stop, return current response
// - Err(...) - return an error
redirect.follow()
}))
.build()?;
// Common use cases for custom:
// 1. Domain restriction
Policy::custom(|redirect| {
if redirect.previous_url().host() == redirect.url().host() {
redirect.follow()
} else {
redirect.stop()
}
})
// 2. Logging/monitoring
Policy::custom(|redirect| {
log_redirect(redirect.previous_url(), redirect.url());
redirect.follow()
})
// 3. Security policies
Policy::custom(|redirect| {
if is_safe_url(redirect.url()) {
redirect.follow()
} else {
redirect.stop()
}
})
// 4. Conditional limits
Policy::custom(|redirect| {
if redirect_count < MAX_REDIRECTS {
redirect_count += 1;
redirect.follow()
} else {
redirect.stop()
}
})
// When to use limited:
// - Standard HTTP clients
// - Simple redirect following
// - Count-based protection against loops
// When to use custom:
// - Security-sensitive applications
// - Need to log/audit redirect chains
// - Domain-specific redirect rules
// - Preventing SSRF via redirects
// - Authentication during redirects
// - HTTPS enforcementKey insight: Policy::limited is the simple choice for count-based redirect limiting—it follows up to N redirects and stops. Policy::custom is for scenarios where you need to inspect each redirect and make decisions based on the URLs, status codes, or application-specific rules. Use custom for security-sensitive applications where redirect chains could lead to SSRF vulnerabilities, when you need to log redirect behavior, or when redirect behavior should vary based on domain or status code. The custom policy receives a Redirect struct with previous_url(), url(), and status() methods, returning follow(), stop(), or an error to control behavior.
