{"tool_call":"file.create","args":{"content":"# What are the differences between reqwest::redirect::Policy::none and reqwest::redirect::Policy::limited for HTTP redirect handling?

Policy::none disables all automatic redirect following, causing the client to return the redirect response directly without following the Location header. Policy::limited(n) allows following redirects up to a maximum count of n, protecting against infinite redirect loops. The key distinction is that none gives you complete control over redirect handling while limited provides automatic following with a safety limit. Use none when you need to inspect redirect responses or implement custom redirect logic, and limited for standard HTTP clients that should follow redirects automatically but need protection from redirect loops.

Basic redirect::Policy::none Usage

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client with no redirect following
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    // Make request to URL that redirects
    let response = client.get(\"https://httpbin.org/redirect/1\")
        .send()
        .await?;
    
    // Response is the redirect itself, not the final destination
    println!(\"Status: {}\", response.status()); // 302 Found
    println!(\"Location: {:?}\", response.headers().get(\"location\"));
    
    // Must manually follow if desired
    if let Some(location) = response.headers().get(\"location\") {
        let final_url = location.to_str()?;
        println!(\"Redirect to: {}\", final_url);
        // Manual follow would require another request
    }
    
    Ok(())
}

Policy::none() returns redirect responses directly without following.

Basic redirect::Policy::limited Usage

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client with limited redirect following
    let client = Client::builder()
        .redirect(Policy::limited(5))
        .build()?;
    
    // Make request to URL that redirects
    let response = client.get(\"https://httpbin.org/redirect/3\")
        .send()
        .await?;
    
    // Response is the final destination after following redirects
    println!(\"Status: {}\", response.status()); // 200 OK
    println!(\"Final URL: {}\", response.url()); // Shows final URL
    
    // Up to 5 redirects followed automatically
    Ok(())
}

Policy::limited(n) follows redirects automatically up to n times.

Default Client Behavior

use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Default client uses Policy::limited(10)
    let client = Client::new();
    
    // Equivalent to:
    let client = Client::builder()
        .redirect(reqwest::redirect::Policy::limited(10))
        .build()?;
    
    // Both follow up to 10 redirects automatically
    let response = client.get(\"https://httpbin.org/redirect/5\")
        .send()
        .await?;
    
    // Response is after following redirects
    println!(\"Status: {}\", response.status());
    
    Ok(())
}

The default client uses Policy::limited(10).

Redirect Loop Protection

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // limited protects against infinite loops
    let client = Client::builder()
        .redirect(Policy::limited(3))
        .build()?;
    
    // URL that redirects infinitely (a -> b -> a -> b -> ...)
    let result = client.get(\"https://httpbin.org/redirect/infinite\")
        .send()
        .await;
    
    // Returns error after 3 redirects
    match result {
        Err(e) => {
            if e.is_redirect() {
                println!(\"Too many redirects: {}\", e);
            }
        }
        Ok(_) => println!(\"Success\"),
    }
    
    // With none, you'd get each redirect response
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let response = client.get(\"https://httpbin.org/redirect/infinite\")
        .send()
        .await?;
    
    // Returns first redirect, doesn't follow
    println!(\"Status: {}\", response.status()); // 302
    
    Ok(())
}

limited protects against loops; none returns each redirect response.

Inspecting Redirect Responses

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Use none to inspect redirect details
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let response = client.get(\"https://httpbin.org/redirect-to?url=/get\")
        .send()
        .await?;
    
    if response.status().is_redirection() {
        println!(\"Redirect status: {}\", response.status());
        
        // Inspect Location header
        if let Some(location) = response.headers().get(\"location\") {
            println!(\"Redirects to: {}\", location.to_str()?);
        }
        
        // Inspect other headers
        println!(\"Content-Length: {:?}\", response.headers().get(\"content-length\"));
        
        // Decide whether to follow
        let should_follow = true; // Your logic here
        if should_follow {
            let location = response.headers().get(\"location\")
                .and_then(|h| h.to_str().ok())
                .unwrap_or(\"/\");
            let final_response = client.get(location)
                .send()
                .await?;
            println!(\"Final status: {}\", final_response.status());
        }
    }
    
    Ok(())
}

none allows inspecting redirect responses before deciding to follow.

Custom Redirect Logic

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    // Implement custom redirect handling
    fn should_follow_redirect(original_url: &str, redirect_url: &str) -> bool {
        // Only follow redirects to same host
        let original_host = url::Url::parse(original_url)
            .ok()
            .and_then(|u| u.host_str().map(|h| h.to_string()));
        let redirect_host = url::Url::parse(redirect_url)
            .ok()
            .and_then(|u| u.host_str().map(|h| h.to_string()));
        
        original_host.is_some() && original_host == redirect_host
    }
    
    let url = \"https://example.com/some/path\";
    let mut current_url = url.to_string();
    let max_redirects = 5;
    
    for _ in 0..max_redirects {
        let response = client.get(&current_url).send().await?;
        
        if !response.status().is_redirection() {
            println!(\"Final response: {}\", response.status());
            break;
        }
        
        let location = response.headers()
            .get(\"location\")
            .and_then(|h| h.to_str().ok())
            .unwrap_or(\"/\");
        
        if !should_follow_redirect(&current_url, location) {
            println!(\"Rejecting redirect to different host: {}\", location);
            break;
        }
        
        println!(\"Following redirect: {} -> {}\", current_url, location);
        current_url = location.to_string();
    }
    
    Ok(())
}

none enables custom redirect policies with full control.

Redirect Count Limits

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Different limits for different use cases
    
    // Strict limit for API calls
    let api_client = Client::builder()
        .redirect(Policy::limited(3))
        .build()?;
    
    // Generous limit for web crawling
    let crawl_client = Client::builder()
        .redirect(Policy::limited(20))
        .build()?;
    
    // No redirects for redirect detection
    let detection_client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    // Use appropriate client for use case
    Ok(())
}

Choose limited value based on expected redirect chains.

Error Handling Differences

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // limited: returns error on too many redirects
    let client = Client::builder()
        .redirect(Policy::limited(2))
        .build()?;
    
    // URL that requires 5 redirects
    let result = client.get(\"https://httpbin.org/redirect/5\")
        .send()
        .await;
    
    match result {
        Err(e) => {
            // Error type: reqwest::Error
            if e.is_redirect() {
                println!(\"Too many redirects\");
            }
            // Contains message about redirect limit
        }
        Ok(_) => println!(\"Unexpected success\"),
    }
    
    // none: returns redirect response, no error
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let response = client.get(\"https://httpbin.org/redirect/5\")
        .send()
        .await?;
    
    // Success - we get the redirect response
    println!(\"Status: {}\", response.status()); // 302
    
    Ok(())
}

limited errors on too many redirects; none returns redirect response.

Cross-Origin Redirects

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // With limited, cross-origin redirects are followed
    let client = Client::builder()
        .redirect(Policy::limited(5))
        .build()?;
    
    // Follows redirect even to different domain
    let response = client.get(\"https://example.com/redirect-to-other-domain\")
        .send()
        .await?;
    // Final URL might be on different domain
    
    // With none, you can inspect and validate cross-origin
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let response = client.get(\"https://example.com/redirect-to-other-domain\")
        .send()
        .await?;
    
    if response.status().is_redirection() {
        let location = response.headers()
            .get(\"location\")
            .and_then(|h| h.to_str().ok());
        
        if let Some(loc) = location {
            let redirect_url = url::Url::parse(loc)?;
            let original_url = url::Url::parse(\"https://example.com\")?;
            
            if redirect_url.host() != original_url.host() {
                println!(\"Warning: cross-origin redirect to {}\", loc);
                // Apply security policy
            }
        }
    }
    
    Ok(())
}

none allows validating cross-origin redirects before following.

POST Request Redirects

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // reqwest follows HTTP standards for POST redirects:
    // 301/302: POST -> GET (loses body)
    // 307/308: POST -> POST (preserves method and body)
    
    let client = Client::builder()
        .redirect(Policy::limited(5))
        .build()?;
    
    // POST with redirect
    let response = client
        .post(\"https://example.com/redirect-endpoint\")
        .body(\"request data\")
        .send()
        .await?;
    
    // Behavior depends on redirect status code
    
    // With none, you control POST redirect behavior
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let response = client
        .post(\"https://example.com/redirect-endpoint\")
        .body(\"request data\")
        .send()
        .await?;
    
    if response.status().is_redirection() {
        // Decide how to handle POST redirect
        let status = response.status();
        if status == 307 || status == 308 {
            // Preserve POST method
            println!(\"Preserving POST for redirect\");
        } else {
            // Convert to GET
            println!(\"Converting to GET for redirect\");
        }
    }
    
    Ok(())
}

none gives control over method preservation during POST redirects.

Relative vs Absolute Redirects

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // limited handles both relative and absolute redirects
    let client = Client::builder()
        .redirect(Policy::limited(5))
        .build()?;
    
    // Both relative and absolute Location headers work
    // Relative: Location: /new-path
    // Absolute: Location: https://other.com/new-path
    
    let response = client.get(\"https://example.com/redirect\")
        .send()
        .await?;
    // reqwest resolves relative URLs correctly
    
    // With none, you must resolve relative URLs manually
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let response = client.get(\"https://example.com/old\")
        .send()
        .await?;
    
    if let Some(location) = response.headers().get(\"location\") {
        let location_str = location.to_str()?;
        let base_url = url::Url::parse(\"https://example.com/old\")?;
        let resolved_url = base_url.join(location_str)?;
        println!(\"Resolved redirect: {}\", resolved_url);
    }
    
    Ok(())
}

limited resolves relative URLs; with none you must resolve manually.

Redirect History Tracking

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // With none, you can track redirect history
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let mut redirect_chain: Vec<String> = Vec::new();
    let mut current_url = \"https://example.com/start\".to_string();
    let max_redirects = 10;
    
    redirect_chain.push(current_url.clone());
    
    for _ in 0..max_redirects {
        let response = client.get(&current_url).send().await?;
        
        if !response.status().is_redirection() {
            println!(\"Final URL: {}\", current_url);
            println!(\"Redirect chain: {:?}\", redirect_chain);
            break;
        }
        
        let location = response.headers()
            .get(\"location\")
            .and_then(|h| h.to_str().ok())
            .unwrap_or(\"/\")
            .to_string();
        
        redirect_chain.push(location.clone());
        current_url = location;
    }
    
    // With limited, redirect history is not directly available
    // You only get the final response
    
    Ok(())
}

none allows building redirect history for auditing or logging.

Security Considerations

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Security risks with unlimited redirects:
    // - Redirect loops consuming resources
    // - SSRF via redirect to internal networks
    // - Credential leakage via redirects
    
    // Safer: use limited with reasonable count
    let safe_client = Client::builder()
        .redirect(Policy::limited(5))
        .build()?;
    
    // Or: custom policy with validation
    let secure_client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    // Validate all redirects before following
    fn validate_redirect(original: &url::Url, redirect: &url::Url) -> bool {
        // Only HTTPS
        if redirect.scheme() != \"https\" {
            return false;
        }
        
        // No internal network redirects
        if let Some(host) = redirect.host_str() {
            if host.starts_with(\"10.\")
                || host.starts_with(\"192.168.\")
                || host.starts_with(\"172.16.\")
                || host == \"localhost\"
                || host.starts_with(\"127.\")
            {
                return false;
            }
        }
        
        true
    }
    
    Ok(())
}

none enables security-conscious redirect handling.

Policy::custom for Advanced Cases

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // For custom logic beyond none/limited, use Policy::custom
    let client = Client::builder()
        .redirect(Policy::custom(|attempt| {
            // attempt contains:
            // - url: the redirect target
            // - previous: how many redirects so far
            
            // Example: limit but also validate
            if attempt.previous() > 5 {
                attempt.stop()
            } else {
                let url = attempt.url();
                if url.scheme() == \"https\" {
                    attempt.follow()
                } else {
                    // Don't follow non-HTTPS redirects
                    attempt.stop()
                }
            }
        }))
        .build()?;
    
    // This combines limited's safety with custom validation
    
    Ok(())
}

Policy::custom provides fine-grained control combining aspects of both approaches.

Comparing Response URLs

use reqwest::redirect::Policy;
use reqwest::Client;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // limited: response.url() shows final URL
    let client = Client::builder()
        .redirect(Policy::limited(5))
        .build()?;
    
    let response = client.get(\"https://httpbin.org/redirect/3\")
        .send()
        .await?;
    
    println!(\"Request URL: https://httpbin.org/redirect/3\");
    println!(\"Response URL: {}\", response.url());
    // Response URL is the final destination after redirects
    
    // none: response.url() shows requested URL
    let client = Client::builder()
        .redirect(Policy::none())
        .build()?;
    
    let response = client.get(\"https://httpbin.org/redirect/3\")
        .send()
        .await?;
    
    println!(\"Response URL: {}\", response.url());
    // Response URL is still the original URL
    
    Ok(())
}

limited updates response.url() to final destination; none preserves original.

Use Cases Summary

use reqwest::redirect::Policy;
 
// Use Policy::none when:
// - Detecting redirects for URL shorteners
// - Implementing custom redirect logic
// - Security audits requiring redirect inspection
// - Following specific redirect paths manually
// - Avoiding automatic following of malicious redirects
// - Building redirect chains for logging
// - Handling POST redirects with custom method preservation
 
// Use Policy::limited when:
// - Standard HTTP client behavior
// - Following normal web redirects
// - API calls where redirects should be transparent
// - Simulating browser redirect behavior
// - Simple redirect following with safety limit

Choose based on whether you need redirect inspection or automatic following.

Synthesis

Policy::none characteristics:

  • Returns redirect responses directly
  • No automatic following of Location header
  • No redirect count tracking or limits
  • Full control over redirect handling
  • Requires manual URL resolution for relative paths
  • Useful for redirect detection and custom logic

Policy::limited characteristics:

  • Follows redirects automatically up to n times
  • Returns error on exceeding limit
  • Resolves relative URLs automatically
  • Updates response.url() to final destination
  • Mimics browser/standard HTTP client behavior
  • Protects against infinite loops

Use Policy::none when:

  • Building URL redirect chain analysis
  • Implementing security validation before following
  • Handling POST redirects with custom logic
  • Logging or auditing redirect paths
  • Building web scrapers with specific redirect policies
  • Detecting redirect loops or malicious redirects

Use Policy::limited when:

  • Standard HTTP client behavior needed
  • Redirects should be transparent to application
  • Following typical web redirect patterns
  • Simple API calls with redirect support
  • Default browser-like behavior required

Redirect loop handling:

  • none: Returns redirect response, never loops
  • limited: Returns error after n redirects

Cross-origin handling:

  • none: Can inspect and validate before following
  • limited: Follows automatically without validation

Method preservation:

  • none: Full control over method in redirects
  • limited: Follows HTTP standards (307/308 preserve method)

Key insight: Policy::none and Policy::limited represent two fundamentally different approaches to redirect handling. none gives you the redirect response and lets you decide what to do—it's useful when redirects are part of your application logic, when you need to validate destinations, or when implementing custom redirect behavior. limited handles redirects automatically up to a safety limit—it's appropriate when redirects should be transparent to your application, like standard HTTP client behavior. The default client uses limited(10) because most HTTP clients should follow redirects by default, but a limit prevents infinite loops. Use none for redirect-aware applications (URL shortener resolvers, redirect auditors, security scanners) and limited for redirect-transparent applications (API clients, web scrapers with standard behavior). For cases requiring both automatic following and validation, Policy::custom provides fine-grained control.","path":"/articles/275_reqwest_redirect_policy_none_vs_limited.md"}}