Rust in Everything
基于 tower-http 0.7.1

ServiceBuilderExt 与 ServiceExt

主线位置:不在主线上。这两个 trait 只是换一种写法把 Layer 套上去,生成的类型和手写 .layer(XxxLayer::new()) 完全一样;本页讲它们覆盖了什么、漏了什么,以及两个容易踩的名字冲突。

flowchart TB
  sb["tower::ServiceBuilder"] -->|"ServiceBuilderExt:先写的在外"| t1["Trace<Compression<svc>>"]
  svc["任何服务"] -->|"ServiceExt:先写的在里"| t2["Trace<Compression<svc>>"]
  sb -->|"tower 自带的 .timeout()"| tt["tower::timeout::Timeout"]
  sb -->|".layer(TimeoutLayer)"| ht["tower_http::timeout::Timeout"]

ServiceBuilderExt:26 个方法

ServiceBuilderExt 是给 tower 的 ServiceBuilder 加方法的扩展 trait,整个 trait 挂在 util feature 后面,每个方法再挂在对应模块的 feature 后面:

pub trait ServiceBuilderExt<L>: sealed::Sealed<L> + Sized {
    #[cfg(feature = "propagate-header")]
    fn propagate_header(
        self,
        header: HeaderName,
    ) -> ServiceBuilder<Stack<crate::propagate_header::PropagateHeaderLayer, L>>;
    // ...
}

impl<L> sealed::Sealed<L> for ServiceBuilder<L> {}

Sealed 是私有模块里的 trait,外面实现不了,所以 ServiceBuilderExt 只可能有 ServiceBuilder 这一个实现,以后加方法不算破坏性修改。每个方法都是 self.layer(XxxLayer::...),返回 ServiceBuilder<Stack<XxxLayer, L>>,顺序规则和 tower 自己的快捷方法一样:先写的在最外层(见 tower 卷 ServiceBuilder)。

26 个方法按模块分:

模块方法
set_headeroverride_request_header、append_request_header、insert_request_header_if_not_present,响应侧同名三个
request_idset_request_id、set_x_request_id、propagate_request_id、propagate_x_request_id
sensitive_headerssensitive_headers、sensitive_request_headers、sensitive_response_headers
tracetrace_for_http、trace_for_grpc
normalize_pathtrim_trailing_slash、append_trailing_slash
其余各一个propagate_header、add_extension、map_request_body、map_response_body、compression、decompression、follow_redirects、catch_panic、request_body_limit

没有快捷方法的模块:timeout、cors、csrf、metrics、on_early_drop、set_status、validate_request、auth,以及请求方向的 RequestDecompression(decompression() 加的是解压响应的那个)。这些只能写 .layer(...)。有快捷方法的那些也只覆盖最常见的构造方式:compression() 就是 CompressionLayer::new(),要调压缩级别还得手写 .layer(CompressionLayer::new().quality(...))。

ServiceBuilder::timeout 是 tower 的

漏掉的 timeout 最容易出事。tower 自己的 ServiceBuilder 就有一个 timeout 方法(开了 tower 的 timeout feature 时),axum 项目里 tower 通常开着 full。于是写 ServiceBuilder::new().timeout(...) 能编译,拿到的却是 tower 的 Timeout:

tower-http = { version = "=0.7.1", features = ["util", "timeout", "trace", "compression-gzip"] }
tower = { version = "=0.5.3", features = ["util", "timeout"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "time", "test-util"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
use bytes::Bytes;
use http::{Request, Response, StatusCode};
use http_body_util::Full;
use std::{convert::Infallible, time::Duration};
use tower::{service_fn, ServiceBuilder, ServiceExt};
use tower_http::{timeout::TimeoutLayer, ServiceBuilderExt as _};

async fn slow(_: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
  tokio::time::sleep(Duration::from_secs(5)).await;
  Ok(Response::new(Full::from("done")))
}

fn short(name: &str) -> String {
  let mut out = String::new();
  for part in name.split_inclusive(['<', '>', ',']) {
    out.push_str(part.trim_start().rsplit("::").next().unwrap());
  }
  out
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  let req = || Request::new(Full::default());

  // ServiceBuilder::timeout 来自 tower 自己(tower 的 timeout feature)
  let a = ServiceBuilder::new().timeout(Duration::from_secs(1)).service_fn(slow);
  println!("builder.timeout(..):  {}", std::any::type_name_of_val(&a));
  match a.oneshot(req()).await {
    Ok(res) => println!("  Ok({})", res.status()),
    Err(e) => println!("  Err({e})"),
  }

  // tower-http 的 TimeoutLayer 没有 builder 快捷方法,只能 .layer(..)
  let b = ServiceBuilder::new()
    .layer(TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(1)))
    .service_fn(slow);
  println!("layer(TimeoutLayer):  {}", std::any::type_name_of_val(&b));
  match b.oneshot(req()).await {
    Ok(res) => println!("  Ok({})", res.status()),
    Err(e) => println!("  Err({e:?})"),
  }

  // ServiceBuilderExt:tower-http 加在 ServiceBuilder 上的方法
  let c = ServiceBuilder::new().trace_for_http().compression().service_fn(slow);
  println!("builder ext:          {}", short(std::any::type_name_of_val(&c)));
  // ServiceExt(0.6.4 起):直接在服务上链;只在这个块里导入,原因见下文
  let d = {
    use tower_http::ServiceExt as _;
    service_fn(slow).compression().trace_for_http()
  };
  println!("service ext:          {}", short(std::any::type_name_of_val(&d)));
}

输出(M2 Max、rustc 1.99.0,跑 3 次相同;ext 是实验 crate 的名字):

builder.timeout(..):  tower::timeout::Timeout<tower::util::service_fn::ServiceFn<ext::slow>>
  Err(request timed out)
layer(TimeoutLayer):  tower_http::timeout::service::Timeout<tower::util::service_fn::ServiceFn<ext::slow>>
  Ok(408 Request Timeout)
builder ext:          Trace<Compression<ServiceFn<slow>>,SharedClassifier<ServerErrorsAsFailures>>
service ext:          Trace<Compression<ServiceFn<slow>>,SharedClassifier<ServerErrorsAsFailures>>

前两行都叫 Timeout,一个在 tower::timeout,一个在 tower_http::timeout。tower 的超时返回 Err(Elapsed)(「request timed out」),错误类型变成 BoxError,放进 axum 的 Router::layer 会因为错误不是 Infallible 而编译不过;tower-http 的超时返回 Ok(408),错误类型不变(tower-http 这一族见 超时一族)。两者在 tower 卷 Timeout 和 axum 卷其余常用中间件里各有讲解,这里只提醒:在 ServiceBuilder 上点出来的 .timeout() 不是 tower-http 的。

ServiceExt:在服务上链

最后两行的类型一样,写法顺序却相反:ServiceBuilder 上 .trace_for_http().compression(),先写的 Trace 在外;ServiceExt 上 service_fn(slow).compression().trace_for_http(),每调一次就在当前服务外面再包一层,后写的在外。

ServiceExt 是 0.6.4 加的(#563,CHANGELOG:「The ServiceExt trait for chaining layers onto an arbitrary http service just like ServiceBuilderExt allows for ServiceBuilder」),方法和 ServiceBuilderExt 一一对应,也是 26 个。它的实现只有一行:

impl<T> ServiceExt for T {}

对任何类型都实现,不要求 T 是 Service。

📝NOTE
冷知识:0.6.3 因为这个 trait 被撤回

ServiceExt 第一次出现在 0.6.3。CHANGELOG 里 0.6.3 只有一句:「This release was yanked because its definition of ServiceExt was quite unhelpful, in a way that's very unlikely that anybody would start depending on within the small timeframe before this was yanked, but that was technically breaking to change.」

0.6.3 的定义是 pub trait ServiceExt<R>: sealed::Sealed<R> + tower::Service<R> + Sized,实现是 impl<R, T> ServiceExt<R> for T where T: tower::Service<R>。问题出在类型参数 R:ServeDir::new(".").add_extension(...) 时,ServeDir 对任何 Request<B> 都实现 Service,编译器推不出 R 是哪个,只能让调用方写类型标注。三天后的 #566(「Make type inference work for ServiceExt」)去掉了 R,提交说明写道:要让类型推断成立,就得去掉泛型参数,于是 tower::Service 这个父 trait 也只能去掉,Sealed 随之失去意义;「This means that now you can call all of these methods on anything」。0.7.1 的 service_ext.rs 末尾还留着一个 test_type_inference,内容就是那一行 ServeDir::new(".").add_extension("&'static str")。

「对任何类型都实现」有一个副作用:ServiceBuilder 也是一个类型。把 ServiceBuilderExt 和 ServiceExt 同时在文件顶部导入:

use tower_http::{timeout::TimeoutLayer, ServiceBuilderExt as _, ServiceExt as _};

上面那段程序第 44 行(let c = ...)就编译不过:

error[E0034]: multiple applicable items in scope
  --> src/main.rs:44:33
   |
44 |   let c = ServiceBuilder::new().trace_for_http().compression().service_fn(slow);
   |                                 ^^^^^^^^^^^^^^ multiple `trace_for_http` found
   |
   = note: candidate #1 is defined in an impl of the trait `ServiceBuilderExt` for the type `ServiceBuilder<L>`
   = note: candidate #2 is defined in an impl of the trait `tower_http::ServiceExt` for the type `T`
help: disambiguate the method for candidate #1
   |
44 -   let c = ServiceBuilder::new().trace_for_http().compression().service_fn(slow);
44 +   let c = ServiceBuilderExt::trace_for_http(ServiceBuilder::new()).compression().service_fn(slow);
   |
help: disambiguate the method for candidate #2
   |
44 -   let c = ServiceBuilder::new().trace_for_http().compression().service_fn(slow);
44 +   let c = tower_http::ServiceExt::trace_for_http(ServiceBuilder::new()).compression().service_fn(slow);
   |

ServiceBuilder<L> 上同时有两个 trace_for_http:ServiceBuilderExt 的那个返回加了一层的 builder,ServiceExt 的那个把 builder 本身当成「服务」包进 Trace。后者毫无用处,但类型系统看不出来。所以一个文件里只导入其中一个,或者像上面的程序那样把 ServiceExt 的导入限制在一个块里。

还要注意 tower_http::ServiceExt 和 tower::ServiceExt 同名。两个都要用时,像上面那样把 tower-http 的写成 use tower_http::ServiceExt as _;,只引入方法、不占名字。

小结

  • ServiceBuilderExt 是封闭的扩展 trait,给 ServiceBuilder 加 26 个方法,每个都是 self.layer(XxxLayer::...),先写的在外层。
  • 没有快捷方法:timeout、cors、csrf、metrics、on_early_drop、set_status、validate_request、auth、请求解压;只能 .layer(...)。
  • ServiceBuilder::timeout 是 tower 的,返回 Err(Elapsed);tower-http 的 TimeoutLayer 返回 Ok(408) 或自选状态码。
  • ServiceExt(0.6.4)在服务上链,后写的在外;对任何类型都实现,这是 #566 为了类型推断付出的代价,0.6.3 因为旧定义被撤回。
  • 两个 trait 同时导入时,ServiceBuilder 上的方法调用会报 E0034;只导入一个,或者限制作用域。

💬 关联讨论

发起讨论
加载中...