Rust in Everything
基于 tower-http 0.7.1

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::Error33所有中间件的 Service 实现(timeout 一个文件就有 5 个)
Infallible4ServeDir、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。同样形状的还有:

模块两个变体短路的响应
CorsCorsCall / PreflightCall预检请求的 200
CsrfFuture / Rejected403
ValidateRequestHeaderFuture / Error401、403、406 等
RequestBodyLimitFuture / PayloadTooLarge413
RequestDecompressionInner / Unsupported415
CatchPanicFuture / Panickedcall 本身 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 模块里讲。

📝NOTE
没有蠢问题

问: 那 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 是同一个考虑。

📝NOTE
冷知识: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 的类型)。

💬 关联讨论

发起讨论
加载中...