FollowRedirect 的策略
主线位置:作用点主线的前两个时刻。
call时记下方法、URI、头、扩展,试着留一份 body;响应 future 完成时看状态码,决定再发一次还是把 3xx 交出去。body 流动时它什么都不做。这一页只讲「谁来决定」:Policytrait 和它的组合子。循环本身、301/302/303/307 怎么改方法、流式 body 遇到 307 为什么原样返回,reqwest 卷 重定向已经对着 0.6.11 讲过,这里只链过去。
flowchart TB
fut["ResponseFuture::poll:内层出了响应"] --> st{"301/302/303/307/308"}
st -->|否| out[原样返回]
st -->|是| body{"BodyRepr::take 拿得到 body"}
body -->|否| out
body -->|是| loc{"Location 能解析"}
loc -->|否| out
loc -->|是| red["policy.redirect(attempt)"]
red -->|Stop| out
red -->|Err| err[返回 Err]
red -->|Follow| next["拼下一跳:保存的头和扩展"]
next --> onreq["policy.on_request(req)"]
onreq --> save["过滤后的头和扩展写回,下一跳从这里拼"]
save --> again["Oneshot 发给内层,再 poll 一次"]
Policy:三个方法
Policy<B, E> 的类型参数是请求 body 和错误类型,只有 redirect 必须实现:
pub trait Policy<B, E> {
fn redirect(&mut self, attempt: &Attempt<'_>) -> Result<Action, E>;
fn on_request(&mut self, _request: &mut Request<B>) {}
fn clone_body(&self, _body: &B) -> Option<B> {
None
}
}
redirect:拿到一个Attempt(状态码、下一跳的方法和地址、上一跳的方法和地址),回答Follow、Stop或者一个错误。Stop的意思是「把这个 3xx 交给调用方」,不是失败。on_request:每发一个请求之前改它。注意第一个请求也会经过:FollowRedirect::call里先policy.on_request(&mut req)再发出去。clone_body:怎么复制一份请求 body 留给下一跳。默认None,不复制。
redirect 和 on_request 都是 &mut self,策略可以带状态。call 每次先 self.policy.clone(),一个请求一份,所以计数这类状态只在一条重定向链里累积,不会跨请求。
clone_body 返回 None 并不意味着带 body 的请求一律不能跟随。clone_body 这个自由函数先看大小提示:size_hint().exact() == Some(0) 时直接 B::default(),不问策略。确定为空的 body 用 Default 造一个新的,这是 FollowRedirect 要求 ReqBody: Body + Default 的原因;301/302 把 POST 改成 GET、303 改成 GET 时,body 直接换成 BodyRepr::Empty,也不需要复制。真正需要复制的只有 307/308 原样重发非空 body 的时候。
组合子
Policy 为几样现成的东西实现好了,另有 7 个组合用的类型,都在 follow_redirect/policy/ 下:
| 类型 | redirect | on_request | clone_body |
|---|---|---|---|
Action、Result<Action, E> | 固定返回自己 | 不改 | None |
Limited | 还有额度就减一并 Follow,否则 Stop | 不改 | None |
SameOrigin | 同源才 Follow | 不改 | None |
FilterCredentials | 总是 Follow,顺便记下这一跳要不要过滤 | 过滤头和扩展 | None |
redirect_fn(f) | 调闭包 | 不改 | None |
clone_body_fn(f) | 总是 Follow | 不改 | 调闭包 |
And<A, B>、Or<A, B> | 见下 | 两个都调 | 先问 A,None 再问 B |
PolicyExt 给所有类型加了 .and() 和 .or()。两者只在 redirect 上不同:And 是「A 说 Follow,再问 B」,否则直接用 A 的结果;Or 是「A 说 Stop 或者出错,再问 B」。
on_request 两者都是 A、B 依次调用,不管 redirect 的结果走了哪边。And 的短路只在 redirect 上:A 说 Stop,B 的 redirect 不会被调用,B 里依赖 redirect 记状态的策略(FilterCredentials 就是)这一跳就不会更新。
默认策略就是一个 And(第 199 行):
pub type Standard = And<Limited, FilterCredentials>;
Limited::default() 是 20 次,注释说这是 Firefox 和 Fetch 规范的上限。用完返回 Stop,调用方拿到的是第 21 个 3xx,Ok 而不是错误。想要报错,就像模块文档那样写 Limited::new(10).or(redirect_fn(|_| Err(MyError::TooManyRedirects))):额度内 Limited 说 Follow,Or 不问右边;额度用完 Limited 说 Stop,右边返回错误。reqwest 没用 Standard,而是自己实现了一个策略,默认 10 次、超出报错(reqwest 卷 重定向)。
FilterCredentials
FilterCredentials::new() 的默认值:跨源时过滤(block_cross_origin: true),过滤的是黑名单里的三个头(第 42 行 的 Authorization、Cookie、Proxy-Authorization),同时清空全部扩展(remove_all_extensions: true)。它分两步干活:
fn redirect(&mut self, attempt: &Attempt<'_>) -> Result<Action, E> {
self.blocked = self.block_any
|| (self.block_cross_origin && !eq_origin(attempt.previous(), attempt.location()));
Ok(Action::Follow)
}
fn on_request(&mut self, request: &mut Request<B>) {
if self.blocked {
// 删头:remove_all 清空,否则删黑名单
// 删扩展:remove_all_extensions 时,只把 allow_extension::<T>() 登记过的类型搬过去
}
}
「同源」由 eq_origin 判断:scheme 相同且只能是 http / https,host 字符串相同,端口(缺省按 80 / 443 补)相同。127.0.0.1 和 localhost 是两个源。比较的是相邻两跳,attempt.previous() 是上一跳的地址,不是最初的地址。
可调的有:block_any()(每一跳都过滤)、block_none()、remove_all()(清空全部请求头)、remove_none()、keep_all_extensions()、allow_extension::<T>()(跨源时只留下这些类型)。
0.7.0 的两处变化
reqwest 卷对着 0.6.11,那时扩展不跟随重发,FollowRedirect 每一跳都新建一个没有扩展的请求。0.6.11 的模块文档给的理由是「Extensions 是 !Clone」,这是 http 0.2 时代的说法,http 1 的 Extensions 已经实现了 Clone(放进去的值也要 Clone)。0.7.0 的 #706 一次改了两件事,CHANGELOG 里都标了 breaking:
- 扩展跟着重发。
call时把扩展快照一份(第 259 行起),每一跳都带上。Standard里的FilterCredentials在跨源时把它们清掉;FollowRedirectLayer::preserve_extensions(false)恢复旧行为,同源也不带。 - 过滤逐跳累积。 每一跳拼好请求、调完
on_request之后,把过滤后的头和扩展写回 future 里保存的那一份(第 379 行):
this.policy.on_request(&mut req);
// Carry the filtered headers and extensions forward so anything dropped on this
// hop stays dropped on the next one (e.g. credentials after a cross-origin hop).
*this.headers = req.headers().clone();
*this.extensions = req.extensions().clone();
0.6.11 没有这两行,每一跳都从最初的请求头拼起(0.6.11 第 327 行)。而 FilterCredentials 只比较相邻两跳,于是 a.test → b.test → b.test 这样的链上,第二次到 b.test 时「上一跳也是 b.test」,不过滤,最初发给 a.test 的 Authorization 就被原样发给了 b.test。
下面用一个假的「网络」把这些行为都跑一遍。它是一个 service_fn,按路径回 3xx,并打印每一跳收到的方法、地址、有没有 Authorization、有没有一个自定义扩展、body 长度。同一个程序里用重命名依赖同时引入 0.6.11 作对照:
tower-http = { version = "=0.7.1", features = ["follow-redirect"] }
old = { package = "tower-http", version = "=0.6.11", features = ["follow-redirect"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "1"
http-body = "1"
http-body-util = "0.1"
bytes = "1"
use bytes::Bytes;
use http::{header, Request, Response, StatusCode};
use http_body::Body as _;
use http_body_util::Full;
use std::{
convert::Infallible,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
};
use tower::{ServiceBuilder, ServiceExt};
use tower_http::follow_redirect::{
policy::{self, PolicyExt},
FollowRedirectLayer, RequestUri,
};
static SENT: AtomicUsize = AtomicUsize::new(0);
static QUIET: AtomicBool = AtomicBool::new(false);
#[derive(Clone)]
struct Tag(&'static str);
// 假的「网络」:按路径回 3xx,并打印每一跳收到了什么
async fn net(req: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
SENT.fetch_add(1, Ordering::SeqCst);
if !QUIET.load(Ordering::SeqCst) {
let auth = req.headers().contains_key(header::AUTHORIZATION);
let tag = req.extensions().get::<Tag>().map(|t| t.0).unwrap_or("-");
let len = req.body().size_hint().exact().unwrap_or(0);
println!(" -> {:<4} {:<24} auth={auth:<5} ext={tag:<8} body={len}", req.method(), req.uri().to_string());
}
let path = req.uri().path().to_owned();
let (status, location) = match path.as_str() {
"/start" => (301, "http://b.test/hop".to_owned()),
"/hop" => (302, "/hop2".to_owned()),
"/hop2" => (302, "http://a.test/back".to_owned()),
"/same" => (302, "/same2".to_owned()),
"/form" => (307, "/done".to_owned()),
"/form302" => (302, "/done".to_owned()),
p if p.starts_with("/loop") => (302, format!("/loop{}", p[5..].parse::<u32>().unwrap() + 1)),
_ => (200, String::new()),
};
let mut res = Response::builder().status(StatusCode::from_u16(status).unwrap());
if !location.is_empty() {
res = res.header(header::LOCATION, location);
}
Ok(res.body(Full::default()).unwrap())
}
fn req(method: &str, uri: &str, body: &'static str) -> Request<Full<Bytes>> {
let mut r = Request::builder()
.method(method)
.uri(uri)
.header(header::AUTHORIZATION, "Bearer t")
.body(Full::from(body))
.unwrap();
r.extensions_mut().insert(Tag("trace-id"));
r
}
fn show(res: Response<Full<Bytes>>) {
let last = res.extensions().get::<RequestUri>().map(|u| u.0.to_string()).unwrap_or_default();
println!(" = {} (RequestUri {last})", res.status());
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let standard = ServiceBuilder::new().layer(FollowRedirectLayer::new()).service_fn(net);
println!("[1] Standard, a -> b -> b -> a");
show(standard.clone().oneshot(req("GET", "http://a.test/start", "")).await.unwrap());
println!("[2] 0.6.11 Standard, a -> b -> b -> a");
let old = ServiceBuilder::new().layer(old::follow_redirect::FollowRedirectLayer::new()).service_fn(net);
// 0.6.11 的 RequestUri 是另一个 crate 里的类型,这里只打印状态码
println!(" = {}", old.oneshot(req("GET", "http://a.test/start", "")).await.unwrap().status());
println!("[3] Standard, same origin");
show(standard.clone().oneshot(req("GET", "http://a.test/same", "")).await.unwrap());
println!("[4] preserve_extensions(false), same origin");
let no_ext = ServiceBuilder::new().layer(FollowRedirectLayer::new().preserve_extensions(false)).service_fn(net);
show(no_ext.oneshot(req("GET", "http://a.test/same", "")).await.unwrap());
println!("[5] Standard, POST 302");
show(standard.clone().oneshot(req("POST", "http://a.test/form302", "hello")).await.unwrap());
println!("[6] Standard, POST 307");
show(standard.clone().oneshot(req("POST", "http://a.test/form", "hello")).await.unwrap());
println!("[7] Standard.and(clone_body_fn), POST 307");
let p = policy::Standard::default()
.and::<_, Full<Bytes>, Infallible>(policy::clone_body_fn(|b: &Full<Bytes>| Some(b.clone())));
let cloning = ServiceBuilder::new().layer(FollowRedirectLayer::with_policy(p)).service_fn(net);
show(cloning.oneshot(req("POST", "http://a.test/form", "hello")).await.unwrap());
println!("[8] Limited::new(2), endless chain");
let limited = ServiceBuilder::new().layer(FollowRedirectLayer::with_policy(policy::Limited::new(2))).service_fn(net);
show(limited.oneshot(req("GET", "http://a.test/loop0", "")).await.unwrap());
println!("[9] Standard, endless chain (hops not printed)");
QUIET.store(true, Ordering::SeqCst);
SENT.store(0, Ordering::SeqCst);
show(standard.oneshot(req("GET", "http://a.test/loop0", "")).await.unwrap());
println!(" requests sent: {}", SENT.load(Ordering::SeqCst));
}
输出(M2 Max、rustc 1.99.0,跑 3 次相同):
[1] Standard, a -> b -> b -> a
-> GET http://a.test/start auth=true ext=trace-id body=0
-> GET http://b.test/hop auth=false ext=- body=0
-> GET http://b.test/hop2 auth=false ext=- body=0
-> GET http://a.test/back auth=false ext=- body=0
= 200 OK (RequestUri http://a.test/back)
[2] 0.6.11 Standard, a -> b -> b -> a
-> GET http://a.test/start auth=true ext=trace-id body=0
-> GET http://b.test/hop auth=false ext=- body=0
-> GET http://b.test/hop2 auth=true ext=- body=0
-> GET http://a.test/back auth=false ext=- body=0
= 200 OK
[3] Standard, same origin
-> GET http://a.test/same auth=true ext=trace-id body=0
-> GET http://a.test/same2 auth=true ext=trace-id body=0
= 200 OK (RequestUri http://a.test/same2)
[4] preserve_extensions(false), same origin
-> GET http://a.test/same auth=true ext=trace-id body=0
-> GET http://a.test/same2 auth=true ext=- body=0
= 200 OK (RequestUri http://a.test/same2)
[5] Standard, POST 302
-> POST http://a.test/form302 auth=true ext=trace-id body=5
-> GET http://a.test/done auth=true ext=trace-id body=0
= 200 OK (RequestUri http://a.test/done)
[6] Standard, POST 307
-> POST http://a.test/form auth=true ext=trace-id body=5
= 307 Temporary Redirect (RequestUri http://a.test/form)
[7] Standard.and(clone_body_fn), POST 307
-> POST http://a.test/form auth=true ext=trace-id body=5
-> POST http://a.test/done auth=true ext=trace-id body=5
= 200 OK (RequestUri http://a.test/done)
[8] Limited::new(2), endless chain
-> GET http://a.test/loop0 auth=true ext=trace-id body=0
-> GET http://a.test/loop1 auth=true ext=trace-id body=0
-> GET http://a.test/loop2 auth=true ext=trace-id body=0
= 302 Found (RequestUri http://a.test/loop2)
[9] Standard, endless chain (hops not printed)
= 302 Found (RequestUri http://a.test/loop20)
requests sent: 21
- [1] 与 [2]:0.7.1 一旦在
a.test → b.test这一跳去掉了Authorization,后面一直没有,回到a.test也没有。0.6.11 在b.test → b.test这一跳把a.test的凭据又发给了b.test。扩展在 0.6.11 从第二跳起一直是空的(它不转发扩展)。 - [3] 与 [4]:同源时
Authorization和扩展都跟着走;preserve_extensions(false)只去掉扩展,头不受影响。 - [5] 与 [6]:302 把 POST 改成 GET、body 换成空的,不需要复制,照常跟随。307 要原样重发 5 字节的
Full,Standard不实现clone_body,take()拿不到 body,3xx 原样返回,内层只收到一个请求。Full<Bytes>本身是Clone的,但Policy没有B: Clone的约束,它不知道。 - [7]:
And的clone_body先问Limited、再问FilterCredentials,都是None,最后问到clone_body_fn,拿到一份副本,307 照常跟随,body 重发了。clone_body_fn的redirect永远Follow,放在And的右边不会放宽Standard的判断。 - [8] 与 [9]:额度 2 时发了 3 个请求,第 3 个响应是 302,原样返回。
Standard是 20 次,一共 21 个请求。RequestUri扩展记的是最后一个请求的地址,不是Location指向的地址。
问: 为什么不干脆在 Policy 上要求 B: Clone,省得写 clone_body_fn?
答: 客户端最常见的请求 body 是流(hyper 的 Incoming、reqwest 包装过的流),它们不是 Clone,也不该是:复制一个流意味着把整个 body 缓存在内存里。Policy<B, E> 不对 B 加任何约束,FollowRedirect 才能套在任意 body 类型上;能复制的类型,由使用者明确说「怎么复制」。代价就是 [6]:Full<Bytes> 明明能廉价复制,Standard 也不会去复制它。
小结
Policy<B, E>:redirect决定跟不跟(Stop是把 3xx 交出去,不是错误),on_request在每个请求(包括第一个)发出前改它,clone_body留一份 body;策略按请求克隆,状态只在一条链里累积。- 确定为空的 body 用
Default补,301/302 的 POST 和 303 换成空 body,所以只有 307/308 重发非空 body 时才需要clone_body;Standard不实现它,clone_body_fn补上。 - 组合子:
Limited(默认 20 次,用完返回 3xx)、SameOrigin、FilterCredentials、redirect_fn、clone_body_fn、And/Or;Standard = And<Limited, FilterCredentials>。 FilterCredentials比较相邻两跳,跨源时删Authorization、Cookie、Proxy-Authorization,并清空扩展。- 0.7.0(#706):扩展默认跟着重发,
preserve_extensions(false)关掉;过滤逐跳累积,0.6.11 会在a → b → b的第三跳把a的凭据发给b,0.7.1 不会。
客户端栈里 FollowRedirect 和别的层怎么排,下一章 拼一个客户端栈。