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 enforcement

Key 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.