How does reqwest::redirect::Policy::custom enable fine-grained redirect handling decisions?
reqwest::redirect::Policy::custom accepts a closure that receives each redirect attempt and returns a decision—follow the redirect, stop with an error, or ignore and return the current response—enabling application-specific logic for cross-origin redirects, authentication handling, and redirect loops. The closure receives detailed information about each redirect including the URL, status code, and previous URLs, allowing you to implement complex policies that go beyond the built-in limited and none policies.
Basic Custom Redirect Policy
use reqwest::redirect::Policy;
use reqwest::Url;
fn basic_custom_policy() {
// Create a client with custom redirect handling
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// This closure is called for each redirect
// attempt contains information about the redirect
println!("Redirect from {} to {}",
attempt.previous().last().unwrap(),
attempt.url()
);
// Return a decision: follow, stop, or don't follow
attempt.follow()
}))
.build()
.unwrap();
}A custom policy receives each redirect attempt and decides whether to follow it.
Policy Decision Types
use reqwest::redirect::Policy;
fn policy_decisions() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// Three possible decisions:
// 1. Follow the redirect
// attempt.follow()
// 2. Stop with an error
// attempt.stop()
// 3. Don't follow but return current response
// attempt.do_not_follow()
match attempt.url().scheme() {
"https" => attempt.follow(),
"http" => attempt.do_not_follow(), // Keep HTTP response
_ => attempt.stop(), // Error for other schemes
}
}))
.build()
.unwrap();
}Three decisions are available: follow, stop with error, or skip redirect.
Cross-Origin Redirect Protection
use reqwest::redirect::Policy;
use reqwest::Url;
fn cross_origin_protection() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
let previous = attempt.previous();
let new_url = attempt.url();
// Check if redirect crosses origins
let previous_origin = get_origin(previous.last().unwrap());
let new_origin = get_origin(new_url);
if previous_origin != new_origin {
// Don't follow cross-origin redirects
// Could log or handle specially
eprintln!("Cross-origin redirect blocked: {} -> {}",
previous_origin, new_origin);
return attempt.stop();
}
attempt.follow()
}))
.build()
.unwrap();
}
fn get_origin(url: &Url) -> String {
format!("{}://{}", url.scheme(), url.host_str().unwrap_or(""))
}Block or log cross-origin redirects that could leak credentials.
Redirect Limit with Custom Logic
use reqwest::redirect::Policy;
fn redirect_limit() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// Custom limit based on redirect type
let max_redirects = match attempt.status() {
// Permanent redirects: allow more
301 | 308 => 15,
// Temporary redirects: allow fewer
302 | 303 | 307 => 5,
// Unknown: be conservative
_ => 3,
};
if attempt.previous().len() >= max_redirects {
eprintln!("Too many redirects: {}", attempt.previous().len());
return attempt.stop();
}
attempt.follow()
}))
.build()
.unwrap();
}Implement custom redirect limits based on redirect type.
Preserving Headers on Redirect
use reqwest::redirect::Policy;
use reqwest::header::{AUTHORIZATION, HeaderMap};
fn sensitive_headers() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// Check if redirect is to different host
let previous = attempt.previous().last().unwrap();
let new_url = attempt.url();
if previous.host() != new_url.host() {
// Don't follow redirects to different hosts with sensitive headers
// The client will strip Authorization headers automatically,
// but you can add custom logic here
if new_url.scheme() == "http" {
// Downgrade to HTTP - definitely don't follow with auth
return attempt.stop();
}
}
attempt.follow()
}))
.build()
.unwrap();
// Note: reqwest automatically strips sensitive headers on cross-origin redirects
// But custom policy gives you control over the decision
}Control whether redirects to different hosts should be followed.
Domain Allowlist
use reqwest::redirect::Policy;
use url::Url;
fn domain_allowlist() {
let allowed_domains = ["api.example.com", "cdn.example.com", "auth.example.com"];
let client = reqwest::Client::builder()
.redirect(Policy::custom(move |attempt| {
let new_host = attempt.url().host_str().unwrap_or("");
if allowed_domains.contains(&new_host) {
attempt.follow()
} else {
eprintln!("Redirect to disallowed domain: {}", new_host);
attempt.stop()
}
}))
.build()
.unwrap();
}Only follow redirects to trusted domains.
POST Request Redirect Handling
use reqwest::redirect::Policy;
fn post_redirect_handling() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// RFC 7231 specifies:
// 301/302: Convert POST to GET (historically)
// 303: Always convert to GET
// 307/308: Preserve method (keep POST)
match attempt.status() {
303 => {
// 303 See Other - always GET
// reqwest handles this automatically
attempt.follow()
}
307 | 308 => {
// 307/308 preserve the method
// For POST requests, this keeps POST
attempt.follow()
}
301 | 302 => {
// Historical behavior: POST becomes GET
// You might want to stop for sensitive POSTs
attempt.follow()
}
_ => attempt.follow()
}
}))
.build()
.unwrap();
}Handle different redirect status codes appropriately for POST requests.
Logging Redirect Chains
use reqwest::redirect::Policy;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn logging_redirects() {
let redirect_count = Arc::new(AtomicUsize::new(0));
let count_clone = redirect_count.clone();
let client = reqwest::Client::builder()
.redirect(Policy::custom(move |attempt| {
let current_count = count_clone.fetch_add(1, Ordering::SeqCst);
println!("Redirect #{}: {} -> {}",
current_count + 1,
attempt.previous().last().unwrap(),
attempt.url()
);
// Log redirect details
println!(" Status: {}", attempt.status());
println!(" Method: {:?}", attempt.previous().len());
attempt.follow()
}))
.build()
.unwrap();
}Log each redirect for debugging or auditing purposes.
Detecting Redirect Loops
use reqwest::redirect::Policy;
fn loop_detection() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
let current_url = attempt.url();
// Check if we've been to this URL before
for previous_url in attempt.previous() {
if previous_url == current_url {
eprintln!("Redirect loop detected: {}", current_url);
return attempt.stop();
}
}
// Also check for longer cycles (A -> B -> C -> A)
// Previous contains the full chain
attempt.follow()
}))
.build()
.unwrap();
}Detect and prevent redirect loops by checking visited URLs.
Conditional Redirect Based on Status
use reqwest::redirect::Policy;
fn status_based_policy() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
match attempt.status() {
// Always follow temporary redirects
302 | 303 | 307 => attempt.follow(),
// Be cautious with permanent redirects
301 | 308 => {
// Could log or verify permanent redirects
if attempt.url().scheme() == "https" {
attempt.follow()
} else {
// Don't permanently redirect to HTTP
attempt.stop()
}
}
// Unknown redirect status
_ => attempt.stop()
}
}))
.build()
.unwrap();
}Handle different redirect statuses with different policies.
Preserving Response on Redirect
use reqwest::redirect::Policy;
fn preserve_response() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// do_not_follow returns the redirect response
// instead of following it
// Useful for:
// - Getting redirect URLs without following
// - Handling redirects manually
// - Checking headers on redirect response
if attempt.url().path().starts_with("/api/") {
// Don't follow API redirects automatically
// Return the redirect response
attempt.do_not_follow()
} else {
// Follow other redirects normally
attempt.follow()
}
}))
.build()
.unwrap();
// When do_not_follow is used, the response is the redirect response
// You can still get the Location header manually
}Use do_not_follow to get the redirect response without following.
Comparison with Built-in Policies
use reqwest::redirect::Policy;
fn built_in_policies() {
// Policy::none() - Don't follow any redirects
let client_no_redirects = reqwest::Client::builder()
.redirect(Policy::none())
.build()
.unwrap();
// Policy::limited(n) - Follow up to n redirects (default is 10)
let client_limited = reqwest::Client::builder()
.redirect(Policy::limited(5))
.build()
.unwrap();
// Policy::custom() - Full control
let client_custom = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// Any custom logic here
attempt.follow()
}))
.build()
.unwrap();
// Built-in policies handle common cases:
// - none: Return redirect responses as-is
// - limited: Follow redirects up to a count
// - custom: Full control for specific requirements
}Built-in policies handle common cases; custom policies provide full control.
Accessing Redirect Attempt Information
use reqwest::redirect::Policy;
fn attempt_info() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
// Information available on the attempt:
// The URL being redirected to
let target_url: &reqwest::Url = attempt.url();
// The history of URLs (all previous redirects)
let history: &[reqwest::Url] = attempt.previous();
// The redirect status code (301, 302, etc.)
let status: reqwest::StatusCode = attempt.status();
// The current response that triggered the redirect
// Note: You can't access response body at this point
println!("Redirect {} -> {}", status, target_url);
println!("History length: {}", history.len());
attempt.follow()
}))
.build()
.unwrap();
}The attempt provides URL, status, and history information.
Real-World Example: OAuth Flow
use reqwest::redirect::Policy;
fn oauth_flow() {
// OAuth flows often have specific redirect handling needs
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
let url = attempt.url();
// OAuth callback URLs
if url.path().contains("/callback") {
// Don't follow - return to the callback URL
// The application needs to handle this
return attempt.do_not_follow();
}
// External OAuth providers
if url.host_str() == Some("accounts.google.com") {
// Follow Google OAuth redirects
return attempt.follow();
}
// Be careful with cross-site redirects during OAuth
attempt.stop()
}))
.build()
.unwrap();
}OAuth flows require careful redirect handling for security.
Async Policy with State
use reqwest::redirect::Policy;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
fn policy_with_state() {
let total_redirects = Arc::new(AtomicU64::new(0));
let max_total = 100;
let redirect_count = total_redirects.clone();
let client = reqwest::Client::builder()
.redirect(Policy::custom(move |attempt| {
// Track total redirects across all requests
let current_total = redirect_count.fetch_add(1, Ordering::Relaxed);
if current_total >= max_total {
eprintln!("Global redirect limit reached");
return attempt.stop();
}
attempt.follow()
}))
.build()
.unwrap();
}Share state across redirect decisions using Arc.
Security Considerations
use reqwest::redirect::Policy;
fn security_considerations() {
let client = reqwest::Client::builder()
.redirect(Policy::custom(|attempt| {
let new_url = attempt.url();
// 1. HTTPS downgrade protection
let previous = attempt.previous().last().unwrap();
if previous.scheme() == "https" && new_url.scheme() == "http" {
eprintln!("HTTPS to HTTP redirect blocked");
return attempt.stop();
}
// 2. Sensitive path protection
let sensitive_paths = ["/admin", "/internal", "/debug"];
for path in sensitive_paths {
if previous.path().starts_with(path) {
// Don't leak sensitive URLs
eprintln!("Redirect from sensitive path blocked");
return attempt.stop();
}
}
// 3. Local network protection
if let Some(host) = new_url.host_str() {
if host == "localhost" || host.starts_with("127.") || host.starts_with("192.168.") {
eprintln!("Redirect to local network blocked");
return attempt.stop();
}
}
// 4. Redirect loop detection
if attempt.previous().contains(new_url) {
eprintln!("Redirect loop blocked");
return attempt.stop();
}
attempt.follow()
}))
.build()
.unwrap();
}Implement security policies to prevent common redirect attacks.
Synthesis
Quick reference:
| Method | Behavior |
|---|---|
attempt.follow() |
Follow the redirect |
attempt.stop() |
Stop with error (too many redirects) |
attempt.do_not_follow() |
Don't follow, return current response |
Policy types:
| Policy | Behavior |
|---|---|
Policy::none() |
Never follow redirects |
Policy::limited(n) |
Follow up to n redirects |
Policy::custom(fn) |
Custom decision for each redirect |
Common patterns:
use reqwest::redirect::Policy;
fn patterns() {
// Pattern 1: Limit with logging
Policy::custom(|attempt| {
println!("Redirect: {} -> {}",
attempt.previous().last().unwrap(),
attempt.url()
);
if attempt.previous().len() > 10 {
attempt.stop()
} else {
attempt.follow()
}
});
// Pattern 2: Allowlist domains
Policy::custom(|attempt| {
let allowed = ["api.example.com", "cdn.example.com"];
if allowed.contains(&attempt.url().host_str().unwrap_or("")) {
attempt.follow()
} else {
attempt.stop()
}
});
// Pattern 3: Don't follow to HTTP
Policy::custom(|attempt| {
if attempt.url().scheme() == "http" {
attempt.stop()
} else {
attempt.follow()
}
});
}Key insight: Policy::custom gives you fine-grained control over redirect decisions by passing a closure that receives a RedirectAttempt and returns a decision. The closure is called for each redirect in the chain, giving you access to the target URL (attempt.url()), the redirect status code (attempt.status()), and the full history of URLs (attempt.previous()). This enables you to implement security policies like blocking cross-origin redirects that could leak authentication headers, preventing HTTPS-to-HTTP downgrades, detecting redirect loops, implementing domain allowlists, and handling POST-to-GET method conversions. The three decisions—follow(), stop(), and do_not_follow()—let you either proceed with the redirect, terminate with an error, or return the redirect response without following. Use do_not_follow() when you need the redirect response itself (for example, to extract a Location header for manual handling), use stop() for error conditions like too many redirects or suspicious behavior, and use follow() for normal redirect handling. The built-in Policy::none() and Policy::limited(n) handle common cases, but Policy::custom is essential for security-sensitive applications, OAuth flows, API clients with specific redirect requirements, or any situation where the default behavior isn't appropriate.
