Rust in Everything
基于 tower-http 0.7.1

FollowRedirect 的策略

主线位置:作用点主线的前两个时刻。call 时记下方法、URI、头、扩展,试着留一份 body;响应 future 完成时看状态码,决定再发一次还是把 3xx 交出去。body 流动时它什么都不做。这一页只讲「谁来决定」:Policy trait 和它的组合子。循环本身、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/ 下:

类型redirecton_requestclone_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:

  1. 扩展跟着重发。 call 时把扩展快照一份(第 259 行起),每一跳都带上。Standard 里的 FilterCredentials 在跨源时把它们清掉;FollowRedirectLayer::preserve_extensions(false) 恢复旧行为,同源也不带。
  2. 过滤逐跳累积。 每一跳拼好请求、调完 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 指向的地址。
📝NOTE
没有蠢问题

问: 为什么不干脆在 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 和别的层怎么排,下一章 拼一个客户端栈。

💬 关联讨论

发起讨论
加载中...