Rust in Everything
基于 tower-http 0.7.1

body 模块

主线位置:作用点主线的第三个时刻。这个模块不实现任何中间件,只提供几个 body 类型,给那些要在 body 流动时动手、又不能把 http-body-util 的类型露出去的模块用。

flowchart TB
  hbu["http-body-util:Full、Limited、UnsyncBoxBody"] -->|包一层| tb["tower_http::body:同名三个"]
  tb --> cp["catch_panic:Full、UnsyncBoxBody"]
  tb --> lim["limit:Limited(请求 body)"]
  tb --> rd["请求解压:UnsyncBoxBody"]
  tb --> fs["ServeDir:UnsyncBoxBody"]

为什么包一层

body.rs 开头的模块文档:

All these are wrappers around other body types. You shouldn't have to use them in your code.
Use `http-body-util` instead.

They exist because we don't want to expose types from `http-body-util` in `tower-http`s public
API.

http-body-util 还是 0.1,以后升 0.2 时它的类型就变了。如果 tower-http 的公开签名里直接写着 http_body_util::Limited<B>,那它跟着升级就是一次破坏性修改,用户手里的 0.1 类型和 tower-http 要的 0.2 类型对不上。包一层自己的新类型,http-body-util 就成了实现细节,换版本只改 tower-http 内部。源码地图里那份公开 API 白名单没有 http_body_util,CI 会拦住漏网的类型。

三个包装都是同一个形状:一个 #[pin] inner 字段,poll_frame、is_end_stream、size_hint 用 body_methods! 宏原样转发。

类型包的是DataErrorDefault谁在用
Fullhttp_body_util::Full<Bytes>BytesInfallible有(derive)CatchPanic 的默认 500 响应
Limited<B>http_body_util::Limited<B>B::DataBoxError无RequestBodyLimit 给内层的请求 body
UnsyncBoxBody<D, E>http_body_util::combinators::UnsyncBoxBody<D, E>DE有(D: Buf + 'static)CatchPanic、请求解压、ServeDir 的响应

UnsyncBoxBody 里的「Unsync」是说装箱后的 body 只要求 Send,不要求 Sync。http-body-util 同时有 BoxBody(Send + Sync)和 UnsyncBoxBody(只 Send),tower-http 选后者,能装下的 body 更多。

只在四个 feature 下编译

lib.rs 里 body 模块前面的 cfg:

#[cfg(any(
    feature = "catch-panic",
    feature = "decompression-br",
    feature = "decompression-deflate",
    feature = "decompression-gzip",
    feature = "decompression-zstd",
    feature = "fs",
    feature = "limit",
))]
pub mod body;

这是 0.6.0 的改动(#477,CHANGELOG:「body module is disabled except for catch-panic, decompression-*, fs, or limit features (BREAKING)」),因为这个模块要依赖 http-body-util,没有模块用它时就不该把依赖拉进来。只开 cors、trace 这些 feature 时,tower_http::body 根本不存在。

注意压缩不在列表里:CompressionBody 是 compression 模块自己的类型,不经过这里。limit 的响应 body limit::ResponseBody 也是 limit 模块自己的,这里的 Limited 只用在请求方向。

每加一层,类型就变

全景里说过,每一层都可能把请求或响应的 body 换成自己的包装类型。按条件加层时,这个问题会一层一层冒出来。下面想做的事很简单:配置里打开了限制,就套 RequestBodyLimitLayer,否则不套。

tower-http = { version = "=0.7.1", features = ["limit", "map-response-body"] }
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"

handler 先写成泛型,因为 RequestBodyLimit 会把内层看到的请求 body 换成 tower_http::body::Limited<B>:

// 请求 body 类型也会被层改掉(RequestBodyLimit 把它换成 Limited<B>),所以 handler 写成泛型
async fn echo<B>(req: Request<B>) -> Result<Response<Full<Bytes>>, Infallible>
where
  B: http_body::Body,
  B::Error: Into<BoxError>,
{
  let msg = match req.into_body().collect().await {
    Ok(c) => format!("read {} bytes", c.to_bytes().len()),
    Err(e) => format!("error: {}", e.into()),
  };
  Ok(Response::new(Full::from(msg)))
}

第一次尝试,两个分支直接返回服务:

    let svc = if limit {
      ServiceBuilder::new().layer(RequestBodyLimitLayer::new(4)).service_fn(echo)
    } else {
      service_fn(echo)
    };
error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:27:7
   |
24 |       let svc = if limit {
   |  _______________-
25 | |       ServiceBuilder::new().layer(RequestBodyLimitLayer::new(4)).service_fn(echo)
   | |       --------------------------------------------------------------------------- expected because of this
26 | |     } else {
27 | |       service_fn(echo)
   | |       ^^^^^^^^^^^^^^^^ expected `RequestBodyLimit<ServiceFn<_>>`, found `ServiceFn<fn(_) -> _ {echo::<_>}>`
28 | |     };
   | |_____- `if` and `else` have incompatible types

服务类型不同,这在 tower 里很常见,装箱就行。第二次尝试,两边都 .boxed_clone():

error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:27:7
   |
24 |       let svc = if limit {
   |  _______________-
25 | |       ServiceBuilder::new().layer(RequestBodyLimitLayer::new(4)).service_fn(echo).boxed_clone()
   | |       ----------------------------------------------------------------------------------------- expected because of this
26 | |     } else {
27 | |       service_fn(echo).boxed_clone()
   | |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `ResponseBody<Full<Bytes>>`, found `http_body_util::Full<bytes::Bytes>`
28 | |     };
   | |_____- `if` and `else` have incompatible types
   |
   = note: expected struct `BoxCloneService<Request<_>, Response<ResponseBody<Full<_>>>, _>`
              found struct `BoxCloneService<Request<_>, Response<Full<_>>, _>`

服务装箱了,BoxCloneService 的类型参数里还有响应类型:RequestBodyLimit 的响应 body 是 limit::ResponseBody<Full<Bytes>>(一个枚举,装内层 body 或 413 时那段「length limit exceeded」),不套就是 Full<Bytes>。tower 的 option_layer(Either)也帮不上忙,Either 要求两边的 Response 相同。

要统一的是 body。两边都先把响应 body 擦成同一个类型:

use tower_http::{body::UnsyncBoxBody, limit::RequestBodyLimitLayer, map_response_body::MapResponseBodyLayer};

#[tokio::main(flavor = "current_thread")]
async fn main() {
  for limit in [true, false] {
    let svc = if limit {
      ServiceBuilder::new()
        .layer(MapResponseBodyLayer::new(UnsyncBoxBody::new))
        .layer(RequestBodyLimitLayer::new(4))
        .service_fn(echo)
        .boxed_clone()
    } else {
      ServiceBuilder::new()
        .layer(MapResponseBodyLayer::new(UnsyncBoxBody::new))
        .service_fn(echo::<Full<Bytes>>)
        .boxed_clone()
    };
    let res = svc.oneshot(Request::new(Full::from("hello"))).await.unwrap();
    let status = res.status();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("limit={limit}: {status} {body:?}");
  }
}

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

limit=true: 200 OK b"error: length limit exceeded"
limit=false: 200 OK b"read 5 bytes"

请求没有 Content-Length,RequestBodyLimit 没法当场回 413,只能给 body 套上 Limited,handler 读到第 5 个字节时拿到 length limit exceeded。这个错误由 handler 自己处理成了 200,真正的服务里应该回 413;这一层的行为见 axum 卷其余常用中间件。

UnsyncBoxBody::new 是 0.7.0 才公开的。0.6.x 里 tower_http::body::UnsyncBoxBody 只有一个 pub(crate) fn new,用户能拿到这个类型(CatchPanic、ServeDir 的响应里就是它),却造不出来,想统一类型只能再套一层 http-body-util 的装箱。0.7.0 把内部构造函数改名为 from_inner,新加了公开的 UnsyncBoxBody::new,同时给 ServeDir 的响应 body 加了一个 From 实现,直接取出里面已经装好箱的 body,不再装第二次(serve_dir/mod.rs 第 771 行):

impl From<ResponseBody> for UnsyncBoxBody<Bytes, io::Error> {
    fn from(body: ResponseBody) -> Self {
        body.inner
    }
}

CHANGELOG 把这一条记在 #537 名下,实际合入的提交标题带的是 #682(「feat(body): add UnsyncBoxBody::new constructor and From<ResponseBody> conversion」)。

📝NOTE
没有蠢问题

问: 模块文档说「You shouldn't have to use them in your code. Use http-body-util instead.」,那我上面用 tower_http::body::UnsyncBoxBody 统一类型,是不是用错了?

答: 两种都行。自己的代码里用 http_body_util::combinators::UnsyncBoxBody 完全没问题(四件套里修 Cors 顺序时用的就是它)。用 tower-http 这个的好处只有一个:当分支里有 ServeDir 时,From 能省掉一次装箱。代价是你的代码要求打开 limit、fs 这类 feature 之一,tower_http::body 才存在。

小结

  • body 模块把 http-body-util 的 Full、Limited、UnsyncBoxBody 各包一层,只为了不把那个 0.1 crate 的类型放进公开 API;CI 用外部类型白名单检查。
  • 0.6.0 起只在 catch-panic、decompression-*、fs、limit 下编译;压缩的 body 和 limit 的响应 body 都不在这里。
  • 每加一层,请求或响应的 body 类型就可能变;按条件加层先撞上服务类型不同,装箱后再撞上响应 body 类型不同。
  • 修法是两个分支先用 MapResponseBodyLayer 把 body 擦成同一个类型,再装箱;0.7.0 起 tower_http::body::UnsyncBoxBody::new 可以直接用,ServeDir 的 body 转过去不会二次装箱。

💬 关联讨论

发起讨论
加载中...