Rust in Everything
基于 tower-http 0.7.1

怎么读这一卷

主线位置:两条主线的总览。本页把作用点主线和结局主线各画一遍,再用一段程序把两条线都走一遍。
flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1[01 全景]
  use1 --> use2[02 共同骨架]
  use2 --> use3[06 结局]
  use3 --> use4[常见坑]
  q -->|读懂实现| all[00 到 12 按顺序]

锁定版本

项值
cratetower-http 0.7.1(edition 2018,rust-version = "1.65")
tagtower-http-0.7.1
commitc941451(2026-08-31,「chore(release): prepare 0.7.1 (#729)」)
一起用的tower 0.5.3(tower 卷)、tokio 1.54.1、hyper 1、axum 0.8.9

源码链接都用 tag,形如 https://github.com/tower-rs/tower-http/blob/tower-http-0.7.1/tower-http/src/...#L123。仓库是一个 workspace,crate 在 tower-http/ 子目录里,旁边是 examples/。

写作时 master 比 tag 多 8 个提交,6 个是 CI 依赖的版本号,另外两个改了代码,都还没发版:

  • bd4aa5b(#744):ServeDir 遇到 ENAMETOOLONG(文件名太长)也回 404,之前是 500。
  • 0ee7a5d(#747):CorsLayer::validate() 和 CorsConfigurationError,让非法的 CORS 配置可以不 panic、返回错误。0.7.1 里非法配置只能 panic。

读 master 会看到这两处 0.7.1 没有的代码,行号也会错开。想在本地对照着读:

git clone https://github.com/tower-rs/tower-http
cd tower-http
git checkout tower-http-0.7.1

两条主线

tower 卷沿「就绪」和「请求」两条线读,因为 tower 的中间件大多在 poll_ready 里占资源(许可、令牌、队列位置),背压一路传回调用方。tower-http 不是这样:tag 里非测试代码的中间件,poll_ready 全部原样转发给内层,只有 Cors 在转发前多跑一次配置检查;ServeDir 只在有 fallback 时问 fallback,Redirect 永远就绪。没有一个中间件占资源、做背压,背压留给 tower 自己的 ConcurrencyLimit、Buffer。中间件自己的失败(超时、超限、跨站、不支持的编码)也不走 Err,而是变成一个 HTTP 响应。细节和统计在 Layer、Service、ResponseFuture、ResponseBody。

所以这一卷换两条线来组织。

作用点主线(三个时刻):一个中间件能改写请求或响应的地方只有三处。

flowchart TB
  callNode["call 时:改请求,或者短路"] --> fut["响应 future 完成时:改状态码和头,决定换不换 body"]
  fut --> body["body 流动时:poll_frame 逐帧改写或观察"]
  callNode -->|"短路:不调内层"| ready["直接造一个响应"]
  • call 时:改请求(加头、改 URI、放 extensions),或者短路:不调内层,直接造一个响应,放进 ResponseFuture 的另一个变体。CORS 预检、CSRF 的 403、RequestBodyLimit 按 Content-Length 回的 413 都是短路。
  • 响应 future 完成时:改状态码和响应头,或者决定换不换 body 类型(压缩、解压在这一刻决定);Timeout、CatchPanic 在这里把「没有响应」变成一个响应。
  • body 流动时:poll_frame 每交出一帧,中间件都能看一眼或者改写(压缩、超时、Trace 的 on_body_chunk / on_eos)。

多数模块跨两到三处:压缩在 call 时读 Accept-Encoding、在 future 里选择、在 body 里干活;Trace 三处都有回调。每章开头的「主线位置」写明本模块占了哪几处。

结局主线(一个请求怎么算结束):成功还是失败、正常结束还是被放弃,tower-http 拆成三件独立的事,各由一个模块负责。

flowchart TB
  resp[响应头出来] --> cls{"classify:失败了吗"}
  cls -->|Ready| decided[当场下结论]
  cls -->|RequiresEos| wait[等 body 结束再看 trailers]
  resp --> eos{"什么算结束"}
  eos --> e1["poll_frame 返回 None"]
  eos --> e2["读到 trailers"]
  eos --> e3["is_end_stream() 为真"]
  resp --> drop{"什么算放弃"}
  drop --> d1["future 没出响应就被 drop"]
  drop --> d2["body 没读完就被 drop"]
  • 什么算失败:classify 把判断从 trace 里抽了出来。看完响应头就能下结论的返回 Ready,要等 trailers 的(gRPC 的 grpc-status)返回 RequiresEos。
  • 什么算结束:body 返回 None、交出 trailers、或者 is_end_stream() 为真,Trace 才调 on_eos。
  • 什么算放弃:用 Drop 当信号。InFlightRequests 的计数守卫跟着 body 走,body 被丢掉才减一;OnEarlyDrop 的守卫没被标记完成就在 drop 时回调。

这三件事放在 06 结局。

一段程序走两条线

下面四层从外到里是 Trace、InFlightRequests、OnEarlyDrop、Csrf,最里面是一个 handler。三个请求:正常读完、被 CSRF 短路、读了一帧就丢掉 body。

tower-http = { version = "=0.7.1", features = ["trace", "metrics", "on-early-drop", "csrf"] }
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"
futures = "0.3"
tracing = "0.1"
use bytes::Bytes;
use futures::stream;
use http::{Request, Response};
use http_body::Frame;
use http_body_util::{combinators::UnsyncBoxBody, BodyExt, Full, StreamBody};
use std::{convert::Infallible, time::Duration};
use tower::{BoxError, ServiceBuilder, ServiceExt};
use tower_http::{
  classify::ServerErrorsFailureClass,
  csrf::CsrfLayer,
  metrics::InFlightRequestsLayer,
  on_early_drop::{OnBodyDropFn, OnEarlyDropLayer},
  trace::TraceLayer,
};
use tracing::Span;

type Body = UnsyncBoxBody<Bytes, BoxError>;

// 最内层:/stream 回两个数据帧,其余回一个
async fn handler(req: Request<Full<Bytes>>) -> Result<Response<Body>, Infallible> {
  println!("    handler {} {}", req.method(), req.uri());
  let body: Body = if req.uri().path() == "/stream" {
    let frames = [Ok::<_, BoxError>(Frame::data(Bytes::from("one"))), Ok(Frame::data(Bytes::from("two")))];
    StreamBody::new(stream::iter(frames)).boxed_unsync()
  } else {
    Full::new(Bytes::from("hello")).map_err(|e| match e {}).boxed_unsync()
  };
  Ok(Response::new(body))
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let (in_flight, counter) = InFlightRequestsLayer::pair();
  let svc = ServiceBuilder::new()
    // 作用点:call 时 / 响应 future 完成时 / body 流动时,各有回调
    .layer(
      TraceLayer::new_for_http()
        .on_request(|_: &Request<Full<Bytes>>, _: &Span| println!("  [call]   trace on_request"))
        .on_response(|res: &Response<_>, _: Duration, _: &Span| println!("  [future] trace on_response {}", res.status()))
        .on_body_chunk(|c: &Bytes, _: Duration, _: &Span| println!("  [body]   trace on_body_chunk {:?}", c))
        .on_eos(|_: Option<&http::HeaderMap>, _: Duration, _: &Span| println!("  [body]   trace on_eos"))
        .on_failure(|c: ServerErrorsFailureClass, _: Duration, _: &Span| println!("  [结局]   trace on_failure {c}")),
    )
    // 结局:计数守卫在 body 里,body 被 drop 才减一
    .layer(in_flight)
    // 结局:body 没到 end-of-stream 就被 drop,回调
    .layer(OnEarlyDropLayer::builder().on_body_drop(OnBodyDropFn::new(|req: &Request<Full<Bytes>>| {
      let path = req.uri().path().to_owned();
      move |_: &http::response::Parts| move || println!("  [结局]   body of {path} dropped early")
    })))
    // 短路:跨站的 POST 不调内层,直接 403
    .layer(CsrfLayer::new())
    .service_fn(handler);

  // 1. 正常读完
  println!("GET /ok");
  let res = svc.clone().oneshot(Request::get("/ok").body(Full::default()).unwrap()).await.unwrap();
  println!("  in flight after response head: {}", counter.get());
  let body = res.into_body().collect().await.unwrap().to_bytes();
  println!("  client read {:?}; in flight: {}", body, counter.get());

  // 2. 短路
  println!("POST /form (Sec-Fetch-Site: cross-site)");
  let req = Request::post("/form").header("sec-fetch-site", "cross-site").body(Full::default()).unwrap();
  let res = svc.clone().oneshot(req).await.unwrap();
  drop(res);
  println!("  in flight: {}", counter.get());

  // 3. 读一帧就丢掉
  println!("GET /stream (read one frame, then drop)");
  let res = svc.clone().oneshot(Request::get("/stream").body(Full::default()).unwrap()).await.unwrap();
  let mut body = res.into_body();
  let first = body.frame().await.unwrap().unwrap();
  println!("  client read {:?}; in flight: {}", first.into_data().unwrap(), counter.get());
  drop(body);
  println!("  in flight: {}", counter.get());
}

输出(M2 Max、rustc 1.99.0,跑 3 次相同):

GET /ok
  [call]   trace on_request
    handler GET /ok
  [future] trace on_response 200 OK
  in flight after response head: 1
  [body]   trace on_body_chunk b"hello"
  [body]   trace on_eos
  client read b"hello"; in flight: 0
POST /form (Sec-Fetch-Site: cross-site)
  [call]   trace on_request
  [future] trace on_response 403 Forbidden
  in flight: 0
GET /stream (read one frame, then drop)
  [call]   trace on_request
    handler GET /stream
  [future] trace on_response 200 OK
  [body]   trace on_body_chunk b"one"
  client read b"one"; in flight: 1
  [结局]   body of /stream dropped early
  in flight: 0

逐段对应:

  • GET /ok:三个时刻依次出现。[call] 在 Trace::call,[future] 在 ResponseFuture::poll,[body] 在 ResponseBody::poll_frame。响应头已经交给调用方时在途数还是 1,body 读完、被 collect 消耗掉以后才回到 0:InFlightRequests 在 future 完成时把计数守卫移进了 body。
  • POST /form:handler 没有运行。Csrf::call 在 call 时就判定这是跨站的写请求,没调内层,直接把 403 放进 ResponseFuture 的 Rejected 变体。外层的 Trace 看到的是一个普通的 403,new_for_http 只把 5xx 算失败,所以没有 on_failure。
  • GET /stream:读了一帧就丢掉 body。Trace 没有 on_eos,因为 body 没有结束;OnEarlyDropBody 的守卫没被标记完成,在 drop 时回调;计数守卫也随 body 一起被 drop,在途数回到 0。

同一个请求,Trace 说「没结束」,OnEarlyDrop 说「被放弃了」,InFlightRequests 说「不在途了」。三个模块各回答结局主线上的一个问题,互不知道对方。

与 axum、reqwest 两卷的分工

axum 卷的 tower-http 部分对着 0.7.1 讲了 Web 应用最常用的几样怎么用、在 axum 里会怎样,示例都跑过;reqwest 卷对着 tower-http 0.6.11 讲了 FollowRedirect 的循环和 Decompression 的字节匹配。这一卷不重写它们,遇到时一句话加链接:

主题已在哪里讲过本卷讲什么
压缩:协商、DefaultPredicate、改哪些头、流式 flushaxum 卷压缩压缩、解压、ServeDir 三方共用的编码内核;body 包装的错误回传
ServeDir / ServeFile 全流程axum 卷ServeDir 与 ServeFile0.7.0 的 Backend trait;Redirect 服务
TimeoutLayer、RequestBodyLimitLayer、CORS 预检、TraceLayer 五个回调、CatchPanicLayeraxum 卷其余常用中间件Timeout 一族;CORS 的配置怎么变成头;Trace 的类型参数与 classify;CatchPanic 的两次捕获
FollowRedirect 的循环、方法改写、跨域去头reqwest 卷重定向Policy 的组合子;0.7.0 的扩展转发与累积过滤
Decompression 按字节匹配reqwest 卷响应0.7.x 的结尾逻辑;服务端方向的 RequestDecompression

阅读路线

  • 只想会用:读 01 全景、02 共同骨架、06 结局,再加后面的「拦请求」和「常见坑」两部分。读完能看懂 tower-http 中间件的类型、知道层的顺序为什么会让代码编译不过,也知道一个请求「失败」「结束」「被放弃」分别由谁判断。
  • 想读懂实现:按顺序读。02 是所有模块页的前提;03 到 08 每个模块一页,开头标出它占了三个时刻里的哪几处。

摘自 tower-http 的代码片段保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的示例用 2 空格缩进。每个示例上方写出它需要的 feature,只开用到的那几个。

前置知识

概念在哪里讲过本卷用到它的地方
Service、poll_ready 与 calltower 卷 Service trait每个中间件都是一个包着内层的 Service
Layer、Stacktower 卷 Layer 与 Stack每个模块都有一个 XxxLayer
ServiceBuilder 的顺序tower 卷 ServiceBuilder先写的在最外层;ServiceBuilderExt 加的方法也一样
Body::poll_frame、Frame、size_hint、is_end_streamhyper 卷 Service 与 Body第三个时刻全在这里;结局主线的「什么算结束」
span、Span::currenttracing 卷 Span 与 EnteredTrace 造的请求 span;OnEarlyDrop 为什么要放在 Trace 里面
Sleeptokio 卷 Sleep 的一生Timeout 一族;InFlightRequests 的定期上报

目录

部分内容
00 导读本页;源码地图
01 全景tower-http 解决什么问题;一个请求穿过三个时刻
02 共同骨架四件套与三个宏;ServiceBuilderExt 与 ServiceExt;body 模块
03 改请求set_header 一族、请求标识、小型改写层、校验与鉴权
04 拦请求CORS 的内部、CSRF、body 大小限制
05 改 body编码内核、压缩、响应解压、请求解压、超时一族
06 结局classify、Trace 的内部、CatchPanic、InFlightRequests、OnEarlyDrop
07 客户端FollowRedirect 的策略;拼一个客户端栈
08 服务ServeDir 的 Backend trait;Redirect 服务
09 设计取舍八个设计决定:换来什么、付出什么、由谁来补
10 常见坑十一个报错与现象,每个都有最小复现
11 在本站中本站用到的两样与手写的四样;升到 0.7.1 会怎样
12 动手从零写一个跨三个时刻的中间件

小结

  • 全卷锁定 tag tower-http-0.7.1(c941451);tag 之后的 #744、#747 改了 ServeDir 和 CORS,都没发版。
  • tower-http 的中间件把 poll_ready 原样转发,不做背压;自己的失败变成 HTTP 响应,不走 Err。
  • 作用点主线:call 时改请求或短路,响应 future 完成时改状态码、头、body 类型,body 流动时逐帧改写或观察。
  • 结局主线:classify 管失败,Trace 的 body 包装管结束,Drop 守卫管放弃,三者互不依赖。
  • axum 卷、reqwest 卷讲过的行为只链过去;只想会用读 01、02、06,想读懂实现按顺序读。

💬 关联讨论

发起讨论
加载中...