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! 宏原样转发。
| 类型 | 包的是 | Data | Error | Default | 谁在用 |
|---|---|---|---|---|---|
Full | http_body_util::Full<Bytes> | Bytes | Infallible | 有(derive) | CatchPanic 的默认 500 响应 |
Limited<B> | http_body_util::Limited<B> | B::Data | BoxError | 无 | RequestBodyLimit 给内层的请求 body |
UnsyncBoxBody<D, E> | http_body_util::combinators::UnsyncBoxBody<D, E> | D | E | 有(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」)。
问: 模块文档说「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 转过去不会二次装箱。