Layer、Service、ResponseFuture、ResponseBody
主线位置:作用点主线的骨架。三个时刻各对应一件:
call时在Service,响应 future 完成时在ResponseFuture,body 流动时在ResponseBody;Layer只负责把配置交给Service。
flowchart TB L["InFlightRequestsLayer:存一个计数器"] -->|"layer(inner)"| S["InFlightRequests<S>:inner + 计数器"] S -->|"call:计数加一,造守卫"| F["ResponseFuture<F>:inner future + 守卫"] F -->|"poll 完成:守卫移进 body"| B["ResponseBody<B>:inner body + 守卫"] B -->|"drop"| D[守卫 drop,计数减一]
四件套
拿最小的一个完整例子看:metrics/in_flight_requests.rs,去掉注释三百行出头,四件都有。
pub struct InFlightRequestsLayer {
counter: InFlightRequestsCounter,
}
impl<S> Layer<S> for InFlightRequestsLayer {
type Service = InFlightRequests<S>;
fn layer(&self, inner: S) -> Self::Service {
InFlightRequests {
inner,
counter: self.counter.clone(),
}
}
}
pub struct InFlightRequests<S> {
inner: S,
counter: InFlightRequestsCounter,
}
impl<S, R, ResBody> Service<Request<R>> for InFlightRequests<S>
where
S: Service<Request<R>, Response = Response<ResBody>>,
{
type Response = Response<ResponseBody<ResBody>>;
type Error = S::Error;
type Future = ResponseFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<R>) -> Self::Future {
let guard = self.counter.increment();
ResponseFuture {
inner: self.inner.call(req),
guard: Some(guard),
}
}
}
ResponseFuture 和 ResponseBody 都用 pin_project_lite::pin_project! 声明,inner 字段标 #[pin]。ResponseFuture::poll 等内层完成,把守卫取出来塞进 body;ResponseBody 的 poll_frame、is_end_stream、size_hint 全部转发给内层,它存在的意义只是「带着守卫」。
不是每个模块四件都要。只在 call 时动手的模块(改请求、放 extensions),响应原样返回,连 ResponseFuture 都不需要,直接 type Future = S::Future。tag 里这样的 Service 实现有 10 个:NormalizePath、MapRequestBody、SetSensitiveRequestHeaders、AddExtension、SetRequestId、AddAuthorization、SetRequestHeader(单头、多头各一个),以及 RequestBodyTimeout、RequestBodyDeadline(它们只是在 call 时给请求 body 套一层)。要在第二个时刻动手的才有 ResponseFuture;要在第三个时刻动手的才有自己的 body 类型,tag 里有 compression、decompression、trace、metrics、limit、timeout(两种)、on_early_drop 这几处。
poll_ready:全部转发
tag 里非测试代码的中间件,poll_ready 全部是一行 self.inner.poll_ready(cx),只有两处多做了点事:
Cors::poll_ready先调ensure_usable_cors_rules(&self.layer),配置非法(比如允许凭据又允许任意头)就 panic,然后照样转发。ServeDir::poll_ready有 fallback 就问 fallback,没有就Ready;ServeFile、Redirect永远Ready。
没有一个中间件在 poll_ready 里占资源。对照 tower 卷 Service trait 的四条规则:这些中间件不预留任何东西,所以「Ready 之后要保持」「没 call 也要能释放」两条对它们是自动满足的,背压完全取决于内层。
Error:全是 S::Error
type Error = 在 tag 里出现的位置按右边归类(不算文档注释和测试辅助代码):
| 右边 | 个数 | 在哪 |
|---|---|---|
S::Error | 33 | 所有中间件的 Service 实现(timeout 一个文件就有 5 个) |
Infallible | 4 | ServeDir、DefaultServeDirFallback、Redirect(服务,不是中间件),以及 body::Full |
BoxError 或 Box<dyn Error + Send + Sync> | — | 只出现在 body 上:WrapBody、CompressionBody、DecompressionBody、body::Limited、TimeoutBody、DeadlineBody |
| 其余 | — | B::Error 透传的 body,测试里的 mock |
中间件没有一个把错误类型换成 BoxError。它们自己的失败都变成响应:超时是一个状态码自选的空响应,body 超限是 413,跨站是 403,不支持的编码是 415。这和 tower 正好相反,tower 的 Timeout 返回 Err(Elapsed),所以 axum 用 tower 的 Timeout 要配 HandleErrorLayer,用 tower-http 的就不用。body 的错误另当别论:响应头已经发出去了,只能在 body 里报错。
短路:一个枚举装两种结局
要短路的模块,call 有两条路:调内层,或者不调、直接给一个现成的响应。Service::Future 只能是一个类型,所以 ResponseFuture 里放一个枚举。CSRF 的最直白:
pin_project! {
#[project = KindProj]
enum Kind<F>
where
F: Future,
{
Future {
#[pin]
future: F,
},
Rejected {
response: Option<F::Output>,
},
}
}
poll 时 Future 变体转发给内层,Rejected 变体把响应 take() 出来立即返回 Ready。同样形状的还有:
| 模块 | 两个变体 | 短路的响应 |
|---|---|---|
Cors | CorsCall / PreflightCall | 预检请求的 200 |
Csrf | Future / Rejected | 403 |
ValidateRequestHeader | Future / Error | 401、403、406 等 |
RequestBodyLimit | Future / PayloadTooLarge | 413 |
RequestDecompression | Inner / Unsupported | 415 |
CatchPanic | Future / Panicked | call 本身 panic 时的 500 |
短路的响应要有 body,中间件又不知道内层用什么 body 类型,只能要求 ResBody: Default,用 Default::default() 造一个空的。Cors 的 Service 实现:
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for Cors<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
ResBody: Default,
Timeout 不用枚举,它到点时也是 Response::new(B::default()),同样要 Default。
Default 让层的顺序影响编译
问题在于,不是所有响应 body 都实现了 Default。tower-http 自己的 body 包装里,body::Full、body::UnsyncBoxBody 实现了,CompressionBody、DecompressionBody、limit::ResponseBody 在内层 body 实现 Default 时也实现;trace::ResponseBody、metrics::ResponseBody、OnEarlyDropBody、TimeoutBody 没有。所以 Cors、Timeout、ValidateRequestHeader 这类层不能直接套在 Trace、InFlightRequests、OnEarlyDrop 外面:
tower-http = { version = "=0.7.1", features = ["cors", "trace"] }
tower = { version = "=0.5.3", features = ["util"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
use bytes::Bytes;
use http::{Request, Response};
use http_body_util::Full;
use std::convert::Infallible;
use tower::{Service, ServiceBuilder};
use tower_http::{cors::CorsLayer, trace::TraceLayer};
async fn handler(_: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::from("hi")))
}
fn is_service<S: Service<Request<Full<Bytes>>>>(_: &S) {}
fn main() {
// Cors 在外,Trace 在里
let svc = ServiceBuilder::new()
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http())
.service_fn(handler);
is_service(&svc);
}
rustc 1.99.0 的报错(registry 路径已缩短):
error[E0277]: the trait bound `ResponseBody<Full<Bytes>, NeverClassifyEos<_>>: Default` is not satisfied
--> src/main.rs:20:14
|
20 | is_service(&svc);
| ---------- ^^^^ the trait `Default` is not implemented for `ResponseBody<http_body_util::Full<bytes::Bytes>, NeverClassifyEos<ServerErrorsFailureClass>>`
| |
| required by a bound introduced by this call
|
help: the trait `Service<http::Request<ReqBody>>` is conditionally implemented for `Cors<S>`
--> tower-http-0.7.1/src/cors/mod.rs:685:1
|
685 | / impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for Cors<S>
686 | | where
687 | | S: Service<Request<ReqBody>, Response = Response<ResBody>>,
688 | | ResBody: Default,
| |______________-------^
| |
| unsatisfied requirement introduced here: `ResponseBody<http_body_util::Full<bytes::Bytes>, NeverClassifyEos<ServerErrorsFailureClass>>: Default`
= note: required for `Cors<Trace<ServiceFn<fn(Request<Full<Bytes>>) -> _ {handler}>, _>>` to implement `Service<http::Request<http_body_util::Full<bytes::Bytes>>>`
不加 is_service 而直接 .oneshot(req),报的是 E0599「the method oneshot exists for struct Cors<_>, but its trait bounds were not satisfied」,后面跟着几屏类型名,更难看出原因。用一个只要求 Service 的空函数把约束钉住,rustc 才会指出是哪一条 where 没满足。
两种修法,各有后果:
tower-http = { version = "=0.7.1", features = ["cors", "trace", "map-response-body"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "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::{ServiceBuilder, ServiceExt};
use tower_http::{cors::CorsLayer, map_response_body::MapResponseBodyLayer, trace::TraceLayer};
async fn handler(_: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
println!(" handler called");
Ok(Response::new(Full::from("hi")))
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// 修法一:调换顺序,Trace 在外
let a = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.service_fn(handler);
// 修法二:Cors 在外,但先把 Trace 的 body 装箱成实现了 Default 的类型
let b = ServiceBuilder::new()
.layer(CorsLayer::permissive())
.layer(MapResponseBodyLayer::new(|body: tower_http::trace::ResponseBody<_, _>| body.boxed_unsync()))
.layer(TraceLayer::new_for_http())
.service_fn(handler);
let preflight = || {
Request::options("/")
.header(header::ORIGIN, "https://a.example")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
.body(Full::default())
.unwrap()
};
println!("trace outside cors");
let res = a.oneshot(preflight()).await.unwrap();
println!(" {} body type: {}", res.status(), std::any::type_name_of_val(res.body()));
println!("cors outside boxed trace");
let res = b.oneshot(preflight()).await.unwrap();
println!(" {} body type: {}", res.status(), std::any::type_name_of_val(res.body()));
}
输出(跑 3 次相同):
trace outside cors
200 OK body type: tower_http::trace::body::ResponseBody<http_body_util::full::Full<bytes::bytes::Bytes>, tower_http::classify::NeverClassifyEos<tower_http::classify::ServerErrorsFailureClass>>
cors outside boxed trace
200 OK body type: http_body_util::combinators::box_body::UnsyncBoxBody<bytes::bytes::Bytes, core::convert::Infallible>
两种都编译、都回了预检的 200,handler 都没被调用。区别在语义:修法一里 Trace 在外,预检请求也会被记日志;修法二保住了「Cors 在最外层」,代价是每个响应多一次装箱。http_body_util::combinators::UnsyncBoxBody 实现了 Default(造一个空 body),这正是修法二能成立的原因。tower-http 自己也有一个同名的包装,在 body 模块里讲。
问: 那 Trace 的 body 为什么不实现 Default,省得大家麻烦?
答: trace::ResponseBody 里除了内层 body,还有 span、分类器、三个回调和起始时间。Default 要凭空造出这些东西:span 可以是 Span::none(),回调类型却是用户传的任意闭包,没法 Default。即便能造,一个「默认的」Trace body 也不对应任何真实请求,on_eos 触发时报给谁都不对。InFlightRequests 的 body 带着计数守卫,凭空造一个守卫会让计数减到负数。带状态的包装不实现 Default,是有意的。
三个内部宏
macros.rs 里有四个 macro_rules!,其中三个和这一章有关(第四个 rate_limited! 给 ServeDir 的告警限频,放在「服务」一部分的 ServeDir 的 Backend trait)。
define_inner_service_accessors! 展开成 get_ref、get_mut、into_inner 三个方法:
macro_rules! define_inner_service_accessors {
() => {
/// Gets a reference to the underlying service.
pub fn get_ref(&self) -> &S {
&self.inner
}
// ...
pub fn into_inner(self) -> S {
self.inner
}
};
}
它要求结构体里那个字段就叫 inner、类型参数就叫 S,这是一条约定而不是 trait。tag 里调用了 32 次;0.7.0 新加的 Csrf 没调,所以 Csrf 没有这三个方法。
opaque_future! 和 opaque_body! 把一个实现细节类型包成一个新的公开结构体,只转发 poll 或 poll_frame / is_end_stream / size_hint。整个 crate 里两个宏各只用了一次,都在 ServeDir:
opaque_body! {
/// Response body for [`ServeDir`] and [`ServeFile`][super::ServeFile].
#[derive(Default)]
pub type ResponseBody = UnsyncBoxBody<Bytes, io::Error>;
}
ServeDir 的响应 future 是 futures_util::future::Map<..., fn(...) -> ...>(第 584 行),直接写进公开 API 就把 futures-util 的类型暴露了出去,以后换实现就是破坏性修改。包一层新类型,外面只看得到 InfallibleResponseFuture。这和 body 模块不暴露 http-body-util 是同一个考虑。
ServeDir 的 Infallible 是后改的opaque_future! 是 2022 年 12 月的 #283(「Make ServeDir infallible」)加的。在那之前 ServeDir 的错误类型是 io::Error,套进 axum 要先处理错误。#283 把 IO 错误就地变成 500 响应,为了不让错误无声无息地消失,顺手让 fs 依赖了 tracing(提交里的一条是「depend on tracing for fs」),出错时记一条 error 日志。这条依赖后来又在 0.6.9 被去掉了(源码地图里那条「ServeDir 的 IO 错误日志不见了」)。今天的 ServeDir::call 仍然是 try_call 之后接一个 map,把 Err 换成 404 或 500。
小结
- 四件套:
Layer存配置,Service管call时,ResponseFuture管响应 future 完成时,ResponseBody管 body 流动时;只在call时动手的 10 个实现直接用S::Future。 poll_ready全部转发(Cors先检查配置,ServeDir只问 fallback),不占资源,不做背压。- 中间件的
Error全是S::Error(33 处),自己的失败变成响应;BoxError只出现在 body 的错误类型上。 - 短路用
Kind枚举装「内层 future」和「现成响应」,并要求响应 body 实现Default;Trace、InFlightRequests、OnEarlyDrop、TimeoutBody的 body 不实现,所以Cors、Timeout这类层不能直接套在它们外面,要么调换顺序,要么先装箱。 - 三个宏:
define_inner_service_accessors!(32 次,Csrf没用),opaque_future!/opaque_body!(各一次,都在ServeDir,为了不暴露futures-util和http-body-util的类型)。