tower-http 解决什么问题
主线位置:作用点主线的起点。本页回答为什么 tower-http 的中间件只认
http和http-body两个 crate,以及这个选择让后面每一章都要面对的那个代价。
flowchart TB tower["tower:Service<Request>,不知道 HTTP"] --> th["tower-http:Service<http::Request<B>>"] th --> axum["axum Router"] th --> hyper["纯 hyper 服务"] th --> tonic["tonic gRPC"] th --> client["hyper-util 客户端、reqwest 内部"]
问题:tower 不知道 HTTP,框架只认自己
tower 把「收一个请求、异步回一个响应」抽象成 Service<Request>,超时、限流、负载均衡写一次,任何协议都能用。代价是它对请求一无所知:Request 是个泛型参数,tower 的 Timeout 超时了只能返回一个 Elapsed 错误,没法回一个 408;它也读不到请求头,做不了压缩协商、CORS、按状态码记日志。
另一头是各个框架自带的中间件。它们懂 HTTP,但只认自己的类型:
- axum 的
from_fn把内层服务装箱成Next,Next里是一个BoxCloneSyncService<Request, Response, Infallible>,Request和Response都是 axum 的别名,body 固定是axum::body::Body。写出来的中间件只能套在 axum 的服务上。 - actix-web 的中间件实现
Transform<S, ServiceRequest>,内层是Service<ServiceRequest, Response = ServiceResponse<B>>,ServiceRequest是 actix-web 自己的类型,带着连接信息和应用数据。同样只能在 actix-web 的服务端用。
两者都只在服务端。HTTP 客户端要加 User-Agent、跟随重定向、解压响应、按状态码记日志,得另写一套。
做法:只认 http 和 http-body
tower-http 的 crate 文档第一行是:
`async fn(HttpRequest) -> Result<HttpResponse, Error>`
紧接着 第 8 行:
All middleware uses the [http] and [http-body] crates as the HTTP abstractions. That means
they're compatible with any library or framework that also uses those crates, such as
[hyper], [tonic], and [warp].
每个中间件都是 impl<S, ReqBody, ResBody> Service<http::Request<ReqBody>> for Xxx<S>,要求内层是 Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,再按需要对 ReqBody / ResBody 加 http_body::Body 约束。请求头、URI、状态码来自 http,body 的逐帧读取来自 http_body::Body::poll_frame(见 hyper 卷 Service 与 Body)。tower-http 不依赖 hyper,也不依赖任何框架。
下面同一个 CompressionLayer 值,先套在一个纯 tower 的 service_fn 上,再套在 axum 的 Router 上:
tower-http = { version = "=0.7.1", features = ["compression-gzip"] }
tower = { version = "=0.5.3", features = ["util"] }
axum = { version = "=0.8.9", default-features = false }
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};
use http_body_util::{BodyExt, Full};
use std::convert::Infallible;
use tower::{Layer, Service, ServiceExt};
use tower_http::compression::CompressionLayer;
fn short<T>(_: &T) -> String {
// 去掉路径,只留类型名,方便看
let mut out = String::new();
for part in std::any::type_name::<T>().split_inclusive(['<', '>', ',']) {
out.push_str(part.trim_start().rsplit("::").next().unwrap());
}
out
}
async fn hit<S, B, RB>(name: &str, svc: S, req: Request<B>)
where
S: Service<Request<B>, Response = Response<RB>>,
S::Error: std::fmt::Debug,
RB: http_body::Body<Data = Bytes>,
RB::Error: std::fmt::Debug,
{
let res = svc.oneshot(req).await.unwrap();
println!("{name}");
println!(" body type: {}", short(res.body()));
println!(" content-encoding: {:?}", res.headers().get(header::CONTENT_ENCODING));
let bytes = res.into_body().collect().await.unwrap().to_bytes();
println!(" {} bytes on the wire, gzip magic: {:02x?}", bytes.len(), &bytes[..2]);
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let text = "tower-http ".repeat(100);
let layer = CompressionLayer::new();
// 1. 一个纯 tower 服务:请求、响应都是 http 的类型,body 用 http-body-util 的 Full
let t = text.clone();
let plain = tower::service_fn(move |_req: Request<Full<Bytes>>| {
let t = t.clone();
async move { Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(t)))) }
});
let req = Request::get("/").header(header::ACCEPT_ENCODING, "gzip").body(Full::default()).unwrap();
hit("tower service_fn", layer.layer(plain), req).await;
// 2. axum 的 Router:同一个 layer 值
let app = axum::Router::new().route("/", axum::routing::get(move || async move { text }));
let req = Request::get("/").header(header::ACCEPT_ENCODING, "gzip").body(axum::body::Body::empty()).unwrap();
hit("axum Router", layer.layer(app), req).await;
}
输出(M2 Max、rustc 1.99.0,跑 3 次相同):
tower service_fn
body type: CompressionBody<Full<Bytes>>
content-encoding: Some("gzip")
51 bytes on the wire, gzip magic: [1f, 8b]
axum Router
body type: CompressionBody<Body>
content-encoding: Some("gzip")
51 bytes on the wire, gzip magic: [1f, 8b]
1100 字节的文本压成 51 字节,两边结果一样。CompressionLayer 不知道它包的是 axum 还是别的什么,它只看到 http::Request 进、http::Response 出。
代价:body 类型一路传染
注意两行 body type:同一个层,响应 body 的类型一个是 CompressionBody<Full<Bytes>>,一个是 CompressionBody<Body>。中间件不知道内层用什么 body,只能把它当类型参数收下,再把自己的包装类型交出去。每多套一层,响应 body 就多包一层:
flowchart TB h["handler:Response<Full>"] --> c["Compression:Response<CompressionBody<Full>>"] c --> t["Trace:Response<trace::ResponseBody<CompressionBody<Full>, ...>>"] t --> m["InFlightRequests:Response<metrics::ResponseBody<...>>"]
请求 body 也一样:RequestBodyLimit 把内层看到的请求 body 换成 Limited<B>,RequestDecompression 换成装箱的 body。这带来三个后果,后面的章节会反复遇到:
- 条件加层会类型不匹配。
if一个分支套了层、另一个分支没套,两边的响应类型不同,编译不过,要装箱或者用Either(body 模块)。 - 层的顺序可能决定能不能编译。有些层要求内层的响应 body 实现
Default(它们短路时要造一个空 body),而Trace的 body 包装没有实现Default。纯 tower 里把CorsLayer放在TraceLayer外面,编译器会拒绝(四件套)。 - 类型名很长。报错信息里的
Trace<Compression<...>, SharedClassifier<...>, ...>常常写满一屏。
axum 让人少碰到这些,是因为 Router::layer 把每一层的响应都转回 axum::body::Body(见 axum 卷 layer 与 route_layer)。
两侧都能用
客户端也是 Service<http::Request<B>>:hyper-util 的 legacy Client 实现了 tower 的 Service,reqwest 内部也是一摞 tower 层。tower-http 的 crate 文档给了一个客户端示例:TraceLayer(用 StatusInRangeAsFailures::new(400..=599),客户端把 4xx 也算失败)、SetRequestHeaderLayer(User-Agent)、DecompressionLayer 套在 Client 外面。
不是每个模块两侧都有意义:
| 只在服务端 | 只在客户端 | 两侧都能用 |
|---|---|---|
Cors、Csrf、Compression、RequestDecompression、RequestBodyLimit、CatchPanic、NormalizePath、ServeDir | FollowRedirect、Decompression(解压响应)、AddAuthorization | Trace、SetRequestHeader / SetResponseHeader、Timeout、SetRequestId、PropagateHeader、InFlightRequests、SetSensitiveHeaders |
压缩和解压最能说明「方向」:
flowchart TB
subgraph res[响应 body]
rc["压缩:服务端 Compression"]
rd["解压:客户端 Decompression"]
end
subgraph req[请求 body]
qc["压缩:没有"]
qd["解压:服务端 RequestDecompression"]
end
四个方向只实现了三个:服务端压缩响应、客户端解压响应、服务端解压请求,没有客户端压缩请求。
问: 为什么偏偏少了请求压缩?
答: 响应压缩有协商:客户端用 Accept-Encoding 说自己能解什么,服务端挑一个。请求方向没有对应的预先协商,客户端发请求之前不知道服务端认不认 gzip,压了可能换来一个 415。tower-http 的 RequestDecompression 遇到不认识的编码就回 415,并在响应里带上 Accept-Encoding 告诉客户端自己支持什么(decompression/request/future.rs 第 84 行),这是事后的协商。真要压缩请求,可以用 MapRequestBody 自己包一层编码器,tower-http 没有现成的。
小结
- tower 的中间件不懂 HTTP,框架的中间件只认本框架的类型;tower-http 只认
http和http-body,所以 axum、hyper、tonic 和 HTTP 客户端都能用。 - 同一个
CompressionLayer套在纯 tower 服务和 axumRouter上行为一样,响应 body 类型分别是CompressionBody<Full<Bytes>>和CompressionBody<Body>。 - 代价是 body 类型层层传染:条件加层要装箱,层的顺序可能影响能不能编译,报错类型很长;axum 的
Router::layer替你把 body 转回axum::body::Body。 - 有的模块只在服务端有意义,有的只在客户端,
Trace、设置头、超时两侧都能用。 - 压缩与解压只实现了三个方向,没有请求压缩。