Loading pageā¦
Rust walkthroughs
Loading pageā¦
reqwest::redirect::Policy allow customization of redirect behavior for HTTP clients?reqwest::redirect::Policy controls how an HTTP client handles redirect responses (3xx status codes). The policy determines whether to follow redirects automatically, how many redirects to follow, which status codes trigger redirects, and whether to include sensitive headers like Authorization across origins. The default policy follows up to 10 redirects but excludes cross-origin redirects for sensitive headers. Custom policies allow fine-grained control: you can limit redirect counts, block certain redirects entirely, inspect redirect targets before following them, or implement completely custom redirect logic. This customization is essential for security-sensitive applications where automatic redirects could leak credentials, for APIs that use redirects for flow control, and for clients that need to enforce redirect policies at the application level.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Default client follows up to 10 redirects
let client = Client::new();
// If the server responds with 301, 302, 303, 307, or 308,
// the client automatically follows to the new URL
let response = client.get("https://example.com/redirect")
.send()
.await?;
// response is the final response after following redirects
println!("Final URL: {}", response.url());
println!("Status: {}", response.status());
Ok(())
}The default client follows up to 10 redirects automatically.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Disable all automatic redirects
let client = Client::builder()
.redirect(Policy::none())
.build()?;
let response = client.get("https://example.com/redirect")
.send()
.await?;
// Response is the redirect response itself (3xx status)
println!("Status: {}", response.status()); // e.g., 302 Found
// Access the redirect location
if let Some(location) = response.headers().get("Location") {
println!("Redirect to: {}", location.to_str()?);
}
// You must follow the redirect manually if desired
Ok(())
}Policy::none() disables all automatic redirect following.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Limit to 5 redirects
let client = Client::builder()
.redirect(Policy::limited(5))
.build()?;
let response = client.get("https://example.com/redirect-chain")
.send()
.await?;
// If more than 5 redirects occur, returns an error
// This prevents infinite redirect loops
Ok(())
}Policy::limited(n) follows at most n redirects before returning an error.
use reqwest::{Client, redirect::Policy};
use url::Url;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Custom policy using a closure
let policy = Policy::custom(|attempt| {
let url = attempt.url();
// Log each redirect
println!("Redirect to: {}", url);
// Follow the redirect
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
let response = client.get("https://example.com/start")
.send()
.await?;
Ok(())
}Policy::custom accepts a closure called for each redirect decision.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
// Information available in attempt:
println!("Current URL: {}", attempt.url());
println!("Redirect number: {}", attempt.previous().len());
println!("Status code: {:?}", attempt.status());
// Access previous URLs in the redirect chain
for (i, prev_url) in attempt.previous().iter().enumerate() {
println!("Previous URL {}: {}", i, prev_url);
}
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
let response = client.get("https://example.com/start")
.send()
.await?;
Ok(())
}The attempt object provides access to redirect context.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
let url = attempt.url();
// Block redirects to certain domains
if url.host_str() == Some("malicious.example.com") {
// Stop following redirects, return error
attempt.stop()
} else {
// Follow redirect
attempt.follow()
}
});
let client = Client::builder()
.redirect(policy)
.build()?;
let response = client.get("https://example.com/start")
.send()
.await?;
Ok(())
}attempt.stop() halts redirect following and returns an error.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
// Custom redirect limit
if attempt.previous().len() > 3 {
attempt.stop()
} else {
attempt.follow()
}
});
let client = Client::builder()
.redirect(policy)
.build()?;
// Now follows at most 4 redirects (initial + 3)
let response = client.get("https://example.com/start")
.send()
.await?;
Ok(())
}attempt.previous().len() gives the count of redirects already followed.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// By default, reqwest strips Authorization and Cookie headers
// when following cross-origin redirects
let client = Client::builder()
.redirect(Policy::default())
.build()?;
let response = client
.get("https://example.com/redirect")
.header("Authorization", "Bearer secret-token")
.send()
.await?;
// If redirect goes to different origin:
// - Authorization header is NOT sent to new origin
// - This prevents credential leakage
Ok(())
}Default policy protects against credential leakage on cross-origin redirects.
use reqwest::{Client, redirect::Policy, header::HeaderMap};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
// Check if cross-origin
let is_cross_origin = attempt.previous().last()
.map(|prev| {
prev.host() != attempt.url().host() ||
prev.scheme() != attempt.url().scheme() ||
prev.port() != attempt.url().port()
})
.unwrap_or(false);
if is_cross_origin {
// For cross-origin, you might want to strip sensitive headers
// The default policy does this automatically
println!("Cross-origin redirect detected");
}
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
Ok(())
}Custom policies can implement cross-origin header handling.
use reqwest::{Client, redirect::Policy, StatusCode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
let status = attempt.status();
match status {
// 301, 302, 303: Convert POST to GET by default
StatusCode::MOVED_PERMANENTLY |
StatusCode::FOUND |
StatusCode::SEE_OTHER => {
println!("Redirect {} - method may change to GET", status);
attempt.follow()
}
// 307, 308: Preserve original method
StatusCode::TEMPORARY_REDIRECT |
StatusCode::PERMANENT_REDIRECT => {
println!("Redirect {} - method preserved", status);
attempt.follow()
}
_ => attempt.follow()
}
});
let client = Client::builder()
.redirect(policy)
.build()?;
Ok(())
}Different status codes have different redirect semantics.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
let url = attempt.url();
// Check for redirect loops
for prev_url in attempt.previous() {
if prev_url == url {
// Redirect back to a URL we've already visited
println!("Redirect loop detected!");
return attempt.stop();
}
}
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
// Custom loop detection complements built-in limit
Ok(())
}Custom policies can detect redirect loops by checking visited URLs.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
let url = attempt.url();
// Only follow HTTPS redirects
if url.scheme() != "https" {
println!("Blocking non-HTTPS redirect to: {}", url);
return attempt.stop();
}
// Only follow redirects to specific hosts
if let Some(host) = url.host_str() {
if !host.ends_with(".example.com") {
println!("Blocking redirect to external host: {}", host);
return attempt.stop();
}
}
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
Ok(())
}Enforce security policies by validating redirect targets.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
let current_url = attempt.url();
// Check if this is a cross-origin redirect
if let Some(previous) = attempt.previous().last() {
let same_origin = previous.scheme() == current_url.scheme()
&& previous.host() == current_url.host()
&& previous.port() == current_url.port();
if !same_origin {
println!("Cross-origin redirect:");
println!(" From: {}", previous);
println!(" To: {}", current_url);
// Log or apply special handling for cross-origin redirects
}
}
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
Ok(())
}Detect cross-origin redirects for security auditing.
use reqwest::{Client, redirect::Policy, StatusCode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// By default:
// - 301, 302, 303: POST is converted to GET
// - 307, 308: POST method and body are preserved
let client = Client::new();
// POST with 302 redirect
// -> Converts to GET (body lost)
let response = client
.post("https://example.com/redirect-302")
.body("request body")
.send()
.await?;
// To preserve POST on all redirects:
let policy = Policy::custom(|attempt| {
// Always follow, method is preserved by reqwest
// for 307/308, but 301/302/303 convert POST to GET
attempt.follow()
});
Ok(())
}Status codes 307 and 308 preserve the POST method and body.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
let previous = attempt.previous().last()
.map(|u| u.to_string())
.unwrap_or_else(|| "initial request".to_string());
println!(
"Redirect #{}: {} -> {}",
attempt.previous().len(),
previous,
attempt.url()
);
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
let response = client.get("https://example.com/start")
.send()
.await?;
// Logs:
// Redirect #1: initial request -> https://example.com/redirect1
// Redirect #2: https://example.com/redirect1 -> https://example.com/redirect2
// etc.
Ok(())
}Log redirect chains for debugging or auditing.
use reqwest::{Client, redirect::Policy};
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct BlockedRedirect(String);
impl fmt::Display for BlockedRedirect {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Blocked redirect to: {}", self.0)
}
}
impl Error for BlockedRedirect {}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
let blocked_domains = ["malicious.com", "tracking.example.com"];
if let Some(host) = attempt.url().host_str() {
for blocked in blocked_domains.iter() {
if host == blocked || host.ends_with(&format!(".{}", blocked)) {
return attempt.stop();
}
}
}
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
match client.get("https://example.com/start").send().await {
Ok(response) => println!("Success: {}", response.status()),
Err(e) => println!("Error: {}", e), // Redirect blocked
}
Ok(())
}Return custom errors when blocking redirects.
use reqwest::redirect::Policy;
// Policy::none() - Never follow redirects
let no_redirects = Policy::none();
// All 3xx responses returned directly to caller
// Policy::limited(n) - Follow at most n redirects
let limited = Policy::limited(5);
// Follows up to 5 redirects, error on 6th
// Policy::default() - Follow up to 10 redirects
let default = Policy::default();
// Default behavior, same as no redirect policy specified
// Policy::custom(closure) - Custom logic for each redirect
let custom = Policy::custom(|attempt| {
// Inspect attempt.url(), attempt.status(), attempt.previous()
// Return attempt.follow() or attempt.stop()
attempt.follow()
});Choose the policy method based on your redirect handling needs.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
// Two possible actions:
// 1. attempt.follow() - Follow the redirect
// - Makes request to the new URL
// - Continues redirect chain if needed
// 2. attempt.stop() - Stop following redirects
// - Returns an error to the caller
// - The redirect is not followed
// Decision based on your criteria:
let url = attempt.url();
if url.scheme() == "https" {
attempt.follow()
} else {
attempt.stop()
}
});
let client = Client::builder()
.redirect(policy)
.build()?;
Ok(())
}Each redirect decision returns either follow() or stop().
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Client-level policy applies to all requests
let client = Client::builder()
.redirect(Policy::limited(3))
.build()?;
// All requests from this client use limited(3) policy
let response = client.get("https://example.com/start")
.send()
.await?;
// Per-request redirect control is done via disable_redirects()
// But you can't set a per-request custom policy directly
// Instead, use a different client for different policies
let no_redirect_client = Client::builder()
.redirect(Policy::none())
.build()?;
let response = no_redirect_client.get("https://example.com/start")
.send()
.await?;
Ok(())
}Redirect policy is configured at the client level, not per-request.
use reqwest::{Client, redirect::Policy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let policy = Policy::custom(|attempt| {
// previous() returns the chain of URLs visited so far
let chain: Vec<String> = attempt.previous()
.iter()
.map(|u| u.to_string())
.collect();
println!("Redirect chain: {:?}", chain);
println!("Current target: {}", attempt.url());
// First redirect: previous() is empty
// Second redirect: previous() contains [first_url]
// Third redirect: previous() contains [first_url, second_url]
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
let response = client.get("https://example.com/start")
.send()
.await?;
// After all redirects:
// - response.url() is the final URL
// - redirect chain was logged during processing
Ok(())
}attempt.previous() provides access to the full redirect chain.
use reqwest::{Client, redirect::Policy};
use url::Url;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let allowed_hosts = ["api.example.com", "cdn.example.com"];
let policy = Policy::custom(move |attempt| {
let url = attempt.url();
// Security checks
let host = match url.host_str() {
Some(h) => h,
None => return attempt.stop(),
};
// Only allow HTTPS
if url.scheme() != "https" {
println!("Blocked non-HTTPS redirect: {}", url);
return attempt.stop();
}
// Only allow whitelisted hosts
let allowed = allowed_hosts.iter().any(|&allowed| {
host == allowed || host.ends_with(&format!(".{}", allowed))
});
if !allowed {
println!("Blocked redirect to non-whitelisted host: {}", host);
return attempt.stop();
}
// Limit total redirects
if attempt.previous().len() > 5 {
println!("Too many redirects");
return attempt.stop();
}
attempt.follow()
});
let client = Client::builder()
.redirect(policy)
.build()?;
let response = client.get("https://api.example.com/start")
.send()
.await?;
Ok(())
}Combine multiple security checks in a custom policy.
reqwest::redirect::Policy provides three levels of control over redirect behavior:
Policy::none() disables automatic redirects entirely. Use this when you need to handle redirects manually, such as when building web crawlers that need to track redirect chains, implementing custom redirect logic, or when redirects indicate error conditions that shouldn't be silently followed.
Policy::limited(n) follows up to n redirects. This is a safety net against infinite redirect loops while still allowing legitimate redirect chains. Use this as a middle ground between no redirects and unlimited redirects.
Policy::custom(closure) provides complete control. The closure receives an attempt object with the redirect URL, status code, and previous URLs in the chain. Return attempt.follow() to proceed or attempt.stop() to halt with an error. This enables:
Key insight: The default policy already provides important security protectionsāstripping Authorization and Cookie headers on cross-origin redirects, and limiting total redirects to 10. Custom policies let you go further: enforcing HTTPS, validating redirect targets, detecting suspicious patterns, and implementing application-specific redirect rules. In security-sensitive applications, always validate redirect targets before following them, especially for OAuth flows, API clients with credentials, and web crawlers.