What is the difference between reqwest::redirect::Policy::limited and none for controlling HTTP redirect behavior?

Policy::limited(n) automatically follows up to n redirect responses before returning an error, while Policy::none disables automatic redirect following entirely, returning redirect responses directly to the caller. The limited policy handles the common case of following redirects transparently with a safety cap, while none gives full control to the caller for handling redirects manually or enforcing strict redirect policies.

HTTP Redirects and Why They Matter

use reqwest::redirect::Policy;
 
// HTTP redirects (301, 302, 303, 307, 308) tell clients to:
// - Fetch the requested resource from a different URL
// - Update bookmarks for permanent redirects (301, 308)
// - Follow temporary redirects (302, 303, 307)
 
// Redirects can be:
// - Legitimate: URL shorteners, domain migrations, load balancing
// - Malicious: Redirect loops, phishing, tracking

HTTP redirect responses require client decisions: follow automatically, reject, or handle manually.

The none Policy: Disable All Redirects

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn none_policy() {
    // Create client that doesn't follow any redirects
    let client = Client::builder()
        .redirect(Policy::none())
        .build()
        .unwrap();
    
    // When a redirect response is received (3xx status):
    // - Client returns the redirect response directly
    // - No automatic following
    // - Caller decides what to do
    
    let response = client.get("http://example.com/redirect")
        .send()
        .await
        .unwrap();
    
    // If the server returns 302 Found with Location header:
    // - Response status is 302 (not the final response)
    // - You can inspect Location header
    // - You can manually follow or reject
    
    if response.status().is_redirection() {
        let location = response.headers()
            .get("Location")
            .and_then(|v| v.to_str().ok());
        println!("Redirect to: {:?}", location);
        // Manual follow logic here
    }
}

Policy::none() returns redirect responses directly without automatic following, giving the caller complete control.

The limited Policy: Follow Redirects Up to a Limit

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn limited_policy() {
    // Create client that follows up to 10 redirects
    let client = Client::builder()
        .redirect(Policy::limited(10))
        .build()
        .unwrap();
    
    // When a redirect response is received:
    // - Client automatically follows to the new URL
    // - Continues following until non-redirect response
    // - Or until redirect limit is reached (returns error)
    
    let response = client.get("http://example.com/start")
        .send()
        .await
        .unwrap();
    
    // If server redirects up to 10 times:
    // - Returns the final response
    // If server redirects more than 10 times:
    // - Returns reqwest::Error with redirect loop detection
    
    // Common limit values:
    // - 10: Default for most use cases
    // - 5: Stricter, for untrusted sources
    // - 0: Same as Policy::none (no redirects)
}

Policy::limited(n) automatically follows redirects up to n times, balancing convenience with safety.

Default Behavior and Limits

use reqwest::Client;
use reqwest::redirect::Policy;
 
fn default_behavior() {
    // reqwest's default Client uses Policy::limited(10)
    let default_client = Client::new();
    // Equivalent to:
    let limited_client = Client::builder()
        .redirect(Policy::limited(10))
        .build()
        .unwrap();
    
    // The default 10-redirect limit protects against:
    // - Accidental redirect loops
    // - Malicious servers sending infinite redirects
    // - Resource exhaustion from following too many redirects
    
    // When limit is exceeded:
    // - Returns Error with kind RedirectLoop
    // - Caller must handle the error
}

The default limit of 10 redirects protects against loops and excessive redirects while handling most legitimate cases.

Redirect Loop Detection

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn redirect_loops() {
    let client = Client::builder()
        .redirect(Policy::limited(20))
        .build()
        .unwrap();
    
    // Server configured to redirect A -> B -> A -> B -> ...
    // After 20 redirects, client returns error
    
    match client.get("http://loop.example/start").send().await {
        Ok(response) => {
            println!("Success: {}", response.status());
        }
        Err(e) => {
            if e.is_redirect() {
                println!("Too many redirects: {}", e);
                // Error kind: RedirectLoop or TooManyRedirects
            }
        }
    }
    
    // Policy::none would return first redirect immediately
    // Avoiding any loop issues
}

limited detects and errors on redirect loops, while none avoids loops by not following redirects at all.

Security Implications of Following Redirects

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn security_considerations() {
    // Potential security issues with automatic redirects:
    
    // 1. Cross-protocol redirects
    // HTTPS -> HTTP downgrades security
    // Malicious: https://bank.com -> http://attacker.com/bank
    
    // 2. Redirect to internal networks
    // Public URL redirects to localhost or internal IP
    // Can expose internal services
    
    // 3. Redirect chains for tracking
    // Multiple tracking redirects before final destination
    
    // Policy::none gives control to validate each redirect:
    let strict_client = Client::builder()
        .redirect(Policy::none())
        .build()
        .unwrap();
    
    // Manual redirect handling:
    let response = strict_client.get("https://example.com/start")
        .send()
        .await
        .unwrap();
    
    if response.status().is_redirection() {
        let location = response.headers()
            .get("Location")
            .and_then(|v| v.to_str().ok())
            .unwrap();
        
        // Security checks:
        // - Is new URL HTTPS?
        // - Is new URL in allowed domains?
        // - Is new URL not localhost/internal?
        
        let url = reqwest::Url::parse(location).unwrap();
        if url.scheme() == "https" && is_allowed_domain(&url) {
            // Safe to follow
        } else {
            // Reject redirect
        }
    }
}

Policy::none enables security-conscious redirect handling with validation before following.

When to Use Each Policy

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn policy_selection() {
    // Use Policy::none when:
    
    // 1. Security-sensitive applications
    // - Need to validate each redirect
    // - Avoid HTTPS -> HTTP downgrades
    
    // 2. API clients
    // - Redirects might indicate misconfiguration
    // - Want explicit control over URL handling
    
    // 3. Crawlers/spiders
    // - Track redirect chains
    // - Detect redirect loops
    
    // 4. Testing/debugging
    // - Inspect redirect responses
    // - Verify redirect behavior
    
    let manual_client = Client::builder()
        .redirect(Policy::none())
        .build()
        .unwrap();
    
    // Use Policy::limited when:
    
    // 1. General web requests
    // - URL shorteners
    // - Normal HTTP navigation
    
    // 2. Following known-good redirects
    // - Trusted sources
    // - Expected redirect patterns
    
    // 3. Convenience over control
    // - Want final response directly
    // - Don't need redirect details
    
    let auto_client = Client::builder()
        .redirect(Policy::limited(5))
        .build()
        .unwrap();
}

Choose none for security and control, limited for convenience with known-good sources.

The Custom Policy Option

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn custom_policy() {
    // Policy::custom allows fine-grained control
    let custom = Policy::custom(|attempt| {
        // attempt.url() - the URL being redirected to
        // attempt.previous() - how many redirects so far
        // attempt.status() - the redirect status code
        
        let url = attempt.url();
        
        // Reject cross-protocol redirects
        if url.scheme() != "https" {
            return attempt.stop();
        }
        
        // Reject internal network redirects
        if let Some(host) = url.host_str() {
            if host == "localhost" || host.starts_with("127.") || host.starts_with("192.168.") {
                return attempt.stop();
            }
        }
        
        // Follow up to 10 redirects
        if attempt.previous().len() >= 10 {
            return attempt.stop();
        }
        
        // Otherwise, follow the redirect
        attempt.follow()
    });
    
    let client = Client::builder()
        .redirect(custom)
        .build()
        .unwrap();
}

Policy::custom provides a callback for per-redirect decisions with full context.

Redirect Status Codes

use reqwest::Client;
use reqwest::redirect::Policy;
 
fn redirect_status_codes() {
    // Different redirect types:
    
    // 301 Moved Permanently
    // - Permanent redirect
    // - Browser may cache
    // - POST may become GET (historical behavior)
    
    // 302 Found
    // - Temporary redirect
    // - POST may become GET (historical behavior)
    
    // 303 See Other
    // - Redirect after POST
    // - Always converts to GET
    
    // 307 Temporary Redirect
    // - Preserves request method
    // - POST stays POST
    
    // 308 Permanent Redirect
    // - Permanent redirect
    // - Preserves request method
    
    // reqwest handles all these correctly:
    // - 301, 302, 303: May change POST to GET
    // - 307, 308: Preserve method
    
    // With Policy::none, you see original status code
    // With Policy::limited, you see final response status
}

Different redirect codes have different semantics; none lets you inspect them, limited follows them.

Method Preservation During Redirects

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn method_preservation() {
    let client = Client::builder()
        .redirect(Policy::limited(5))
        .build()
        .unwrap();
    
    // POST to endpoint that returns 307 Temporary Redirect
    // 307 preserves the POST method
    
    let response = client
        .post("http://example.com/create")
        .body("data")
        .send()
        .await
        .unwrap();
    
    // With 307/308: POST is preserved
    // With 301/302/303: POST may become GET
    
    // The redirect policy affects whether this happens automatically
    // With Policy::none: You decide whether to preserve method
}

HTTP 307/308 preserve the request method during redirects; Policy::none lets you control method handling.

Inspecting the Redirect Chain

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn redirect_chain() {
    let client = Client::builder()
        .redirect(Policy::limited(10))
        .build()
        .unwrap();
    
    let response = client.get("http://example.com/start")
        .send()
        .await
        .unwrap();
    
    // With Policy::limited, you get final response
    // Redirect chain is not directly visible
    
    // To track redirect chain:
    let tracking_client = Client::builder()
        .redirect(Policy::none())
        .build()
        .unwrap();
    
    let mut url = "http://example.com/start";
    let mut chain = Vec::new();
    
    loop {
        let response = tracking_client.get(url).send().await.unwrap();
        chain.push(url.to_string());
        
        if response.status().is_redirection() {
            url = response.headers()
                .get("Location")
                .and_then(|v| v.to_str().ok())
                .unwrap();
        } else {
            break;
        }
    }
    
    println!("Redirect chain: {:?}", chain);
}

Policy::none enables tracking the complete redirect chain; limited follows automatically without exposing intermediate steps.

Error Handling Differences

use reqwest::Client;
use reqwest::redirect::Policy;
 
async fn error_handling() {
    let limited_client = Client::builder()
        .redirect(Policy::limited(3))
        .build()
        .unwrap();
    
    let none_client = Client::builder()
        .redirect(Policy::none())
        .build()
        .unwrap();
    
    // With Policy::limited:
    // - Redirect loop returns error
    // - Too many redirects returns error
    // - Invalid redirect URL returns error
    
    match limited_client.get("http://loop.example/").send().await {
        Ok(response) => println!("Final response: {}", response.status()),
        Err(e) => {
            if e.is_redirect() {
                println!("Redirect error: {}", e);
            }
        }
    }
    
    // With Policy::none:
    // - No redirect errors (client doesn't follow)
    // - Returns redirect response as success
    // - Caller handles redirect logic
    
    match none_client.get("http://loop.example/").send().await {
        Ok(response) => {
            if response.status().is_redirection() {
                // It's a redirect, no error
                println!("Redirect: {}", response.status());
            }
        }
        Err(e) => {
            // Other errors (connection, timeout, etc.)
            println!("Error: {}", e);
        }
    }
}

limited can fail with redirect errors; none returns redirect responses as success.

Comparison Summary

use reqwest::redirect::Policy;
 
fn comparison_summary() {
    // | Aspect | Policy::limited(n) | Policy::none |
    // |--------|-------------------|--------------|
    // | Automatic follow | Yes, up to n | No |
    // | Returns | Final response | Redirect response |
    // | Loop detection | Yes (error) | N/A (doesn't follow) |
    // | Security control | Limited | Full |
    // | Use case | General web requests | APIs, security-sensitive |
    
    // | Redirect count | Behavior |
    // |----------------|----------|
    // | 0 | Same as Policy::none |
    // | 1-10 | Common limits |
    // | Unlimited (custom) | Can cause infinite loops |
}

Synthesis

Quick reference:

use reqwest::Client;
use reqwest::redirect::Policy;
 
fn quick_reference() {
    // Policy::limited(n) - Follow redirects automatically up to n times
    let auto_client = Client::builder()
        .redirect(Policy::limited(10))  // Default for Client::new()
        .build()
        .unwrap();
    
    // Policy::none - Don't follow redirects, return redirect response
    let manual_client = Client::builder()
        .redirect(Policy::none())
        .build()
        .unwrap();
    
    // Policy::custom - Per-redirect decision making
    let custom_client = Client::builder()
        .redirect(Policy::custom(|attempt| {
            // Validate URL, check count, etc.
            if attempt.url().scheme() != "https" {
                attempt.stop()  // Reject redirect
            } else if attempt.previous().len() >= 5 {
                attempt.stop()  // Too many redirects
            } else {
                attempt.follow()  // Follow redirect
            }
        }))
        .build()
        .unwrap();
}

Key insight: Policy::limited and Policy::none represent opposite ends of the redirect handling spectrum—limited prioritizes convenience by automatically following redirects with a safety cap to prevent infinite loops, while none prioritizes control by returning redirect responses directly to the caller without automatic following. The limited policy is appropriate for general web requests where redirects are expected and trusted (URL shorteners, HTTP to HTTPS upgrades, domain migrations), while none is essential for security-sensitive applications that need to validate each redirect destination, avoid HTTPS-to-HTTP downgrades, prevent access to internal networks through redirect attacks, or track the complete redirect chain. The limited policy can return redirect-related errors (too many redirects, loops), while none never returns redirect errors because it doesn't follow them—it returns the redirect response as a successful result, making the caller responsible for handling. For fine-grained control with automatic following, Policy::custom provides a callback that can validate each redirect before deciding whether to follow, combining the benefits of both approaches.