Rust in Everything
基于 axum 0.8.9

the trait bound Infallible: From<Box<dyn Error + Send + Sync>> is not satisfied

主线位置:报错出在装配主线,.layer(...) 或 axum::serve(...) 的类型检查;约束的理由在请求主线的末端:服务返回的 Err 一路传到 hyper,连接会被关掉,客户端收不到任何响应。

flowchart TB
  slow["handler 太慢"] --> tmo["tower Timeout"]
  tmo -->|"Err(Elapsed)"| which{"外面有什么"}
  which -->|"HandleErrorLayer"| resp["你的函数生成响应"]
  which -->|"什么都没有"| hyper["传到 hyper:关连接"]
  hyper -.- block["axum 在编译期拦下"]

完整的报错文字是 the trait bound `Infallible: From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not satisfied,标题里省掉了括号和 'static。

复现

给 Router 挂 tower 的 TimeoutLayer:

axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["timeout"] }
use std::time::Duration;

use axum::{routing::get, Router};
use tower::timeout::TimeoutLayer;

fn main() {
  let _app: Router = Router::new()
    .route("/", get(|| async { "ok" }))
    .layer(TimeoutLayer::new(Duration::from_secs(1)));
}

rustc 1.99.0 的报错(只去掉了路径前缀):

error[E0277]: the trait bound `Infallible: From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not satisfied
   --> src/bin/inf_bad.rs:9:12
    |
  9 |     .layer(TimeoutLayer::new(Duration::from_secs(1)));
    |      ----- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not implemented for `Infallible`
    |      |
    |      required by a bound introduced by this call
    |
help: the trait `From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not implemented for `Infallible`
      but trait `From<!>` is implemented for it
   --> core/src/convert/mod.rs:990:1
    |
990 | const impl From<!> for Infallible {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    = help: for that trait implementation, expected `!`, found `Box<(dyn std::error::Error + Send + Sync + 'static)>`
    = note: required for `Box<(dyn std::error::Error + Send + Sync + 'static)>` to implement `Into<Infallible>`
note: required by a bound in `Router::<S>::layer`
   --> axum-0.8.9/src/routing/mod.rs:308:50
    |
303 |     pub fn layer<L>(self, layer: L) -> Router<S>
    |            ----- required by a bound in this associated function
...
308 |         <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
    |                                                  ^^^^^^^^^^^^^^^^ required by this bound in `Router::<S>::layer`

For more information about this error, try `rustc --explain E0277`.

依赖树不同,help 那一段列出的 From 实现会不同:依赖里有 zerocopy 时,它还会列出 zerocopy 给 Infallible 实现的 From<AlignmentError<..>>。不变的是第一行和最后的 note。

读这个报错要抓两处:

  • 第一行里没有 Timeout。 它只说 Box<dyn Error + Send + Sync> 转不成 Infallible。Box<dyn Error + Send + Sync> 就是 tower::BoxError,是 tower 的 Timeout 的错误类型:超时时它返回 Err(Elapsed),装箱成 BoxError。
  • 最后的 note 指向 Router::layer 的第 308 行,Error: Into<Infallible>:套上 layer 之后的服务不能出错。

tower 里错误类型是 BoxError 的中间件不止 Timeout,LoadShed(满载时 Overloaded)、Buffer 也是,挂上去报的是同一个错。ConcurrencyLimit、RateLimit 的错误类型就是内层的错误类型,挂在 Router 上没有这个问题。

同一个约束的另外两种报法

HandleErrorLayer 放反了。 知道要配 HandleErrorLayer,但在 ServiceBuilder 里写在了 TimeoutLayer 后面:

use std::time::Duration;

use axum::error_handling::HandleErrorLayer;
use axum::http::StatusCode;
use axum::{routing::get, BoxError, Router};
use tower::timeout::TimeoutLayer;
use tower::ServiceBuilder;

async fn on_error(_: BoxError) -> StatusCode {
  StatusCode::REQUEST_TIMEOUT
}

fn main() {
  // HandleErrorLayer written second: it ends up inside TimeoutLayer.
  let _app: Router = Router::new().route("/", get(|| async { "ok" })).layer(
    ServiceBuilder::new()
      .layer(TimeoutLayer::new(Duration::from_secs(1)))
      .layer(HandleErrorLayer::new(on_error)),
  );
}

依赖同上(ServiceBuilder 不需要额外的 feature)。报两个错,第二个和上面复现的那个相同,第一个是:

error[E0277]: the trait bound `HandleError<Route, fn(Box<_>) -> _ {on_error}, _>: Service<_>` is not satisfied
   --> src/bin/inf_order_bad.rs:16:5
    |
 15 |     let _app: Router = Router::new().route("/", get(|| async { "ok" })).layer(
    |                                                                         ----- required by a bound introduced by this call
 16 | /     ServiceBuilder::new()
 17 | |       .layer(TimeoutLayer::new(Duration::from_secs(1)))
 18 | |       .layer(HandleErrorLayer::new(on_error)),
    | |_____________________________________________^ the trait `Service<axum::http::Request<Body>>` is not implemented for `HandleError<Route, fn(Box<_>) -> _ {on_error}, _>`
    |
    = help: the following other types implement trait `Service<Request>`:
              HandleError<S, F, ()>
              HandleError<S, F, (T1, T2)>
              HandleError<S, F, (T1, T2, T3)>
              HandleError<S, F, (T1, T2, T3, T4)>
              HandleError<S, F, (T1, T2, T3, T4, T5)>
              HandleError<S, F, (T1, T2, T3, T4, T5, T6)>
              HandleError<S, F, (T1, T2, T3, T4, T5, T6, T7)>
              HandleError<S, F, (T1, T2, T3, T4, T5, T6, T7, T8)>
            and 9 others
    = note: required for `Timeout<HandleError<Route, fn(Box<_>) -> _ {on_error}, _>>` to implement `Service<axum::http::Request<Body>>`
note: required by a bound in `Router::<S>::layer`
   --> axum-0.8.9/src/routing/mod.rs:306:21
    |
303 |     pub fn layer<L>(self, layer: L) -> Router<S>
    |            ----- required by a bound in this associated function
...
306 |         L::Service: Service<Request> + Clone + Send + Sync + 'static,
    |                     ^^^^^^^^^^^^^^^^ required by this bound in `Router::<S>::layer`
    = note: the full name for the type has been written to '.../inf_order_bad-8917bb56037bb881.long-type-1131077078530236604.txt'
    = note: consider using `--verbose` to print the full type name to the console

ServiceBuilder 里先写的在外层(tower 卷 层的顺序反了),所以套出来的是 Timeout<HandleError<Route, ..>>,note 里写得很清楚。HandleError 包的是 Route,Route 的错误类型是 Infallible;HandleError 的 Service 实现 要求 F: FnOnce(S::Error) -> Fut,也就是 on_error 得接受 Infallible,而它接受的是 BoxError,于是第一个错。外面的 Timeout 照样产生 BoxError,于是第二个错。

把整个 Router 包起来,在 axum::serve 才报。 想让超时也管到 404,用 Layer::layer(router) 从外面包:

use std::time::Duration;

use axum::extract::Request;
use axum::{routing::get, Router, ServiceExt};
use tower::timeout::TimeoutLayer;
use tower::Layer;

#[tokio::main]
async fn main() {
  let router = Router::new().route("/", get(|| async { "ok" }));
  // Wrapping the whole Router from outside: compiles until axum::serve.
  let app = TimeoutLayer::new(Duration::from_secs(1)).layer(router);
  let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
  axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await.unwrap();
}

TimeoutLayer::layer(router) 本身没有任何约束,通过了。错误推迟到 axum::serve(tokio 加 macros、rt-multi-thread、net):

error[E0271]: type mismatch resolving `<Timeout<Router> as Service<Request<Body>>>::Error == Infallible`
   --> src/bin/inf_serve_bad.rs:14:3
    |
 14 |   axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await.unwrap();
    |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Infallible`, found `Box<dyn Error + Send + Sync>`
    |
    = note: expected enum `Infallible`
             found struct `Box<(dyn std::error::Error + Send + Sync + 'static)>`
note: required by a bound in `serve`
   --> axum-0.8.9/src/serve/mod.rs:101:46
    |
 97 | pub fn serve<L, M, S>(listener: L, make_service: M) -> Serve<L, M, S>
    |        ----- required by a bound in this function
...
101 |     S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static,
    |                                              ^^^^^^^^^^^^^^^^^^ required by this bound in `serve`

后面还跟着一个 E0277「Serve<..> is not a future」,是同一个原因(Serve 的 IntoFuture 实现带着同样的约束),并建议「remove the .await」。这条建议不对路,问题在错误类型,不在 .await。这一次是 E0271,而且 axum::serve 要的是 Error = Infallible 本身,不是 Router::layer 那样的 Into<Infallible>(为什么 Router::layer 放宽成 Into,见 layer 与 route_layer 最后一节)。

为什么非要 Infallible

axum 的 error_handling.md 开头说:「If you have a Service that produces an error and that error makes it all the way up to hyper, the connection will be terminated without sending a response. This is generally not desirable so axum makes sure you always produce a response by relying on the type system.」handler 返回的 Err(StatusCode) 在 axum 里不算错误,它经 IntoResponse 变成响应;只有 tower 意义上的 Service::Error 才是「错误」,而 axum 不允许这种错误到达 hyper。Router 内部每个服务都是 Route<Infallible>,axum::serve 也只收 Error = Infallible 的服务,中间件带进来的错误必须在两处之一被消化掉。

📝NOTE
冷知识:axum 0.2 可以路由到会出错的服务

#402「Revamp error handling model」(首次发布于 v0.3.0)之前,route("/", some_fallible_service) 和挂会出错的中间件都是允许的,Router 还有一个 check_infallible 方法。PR 说明的理由是:「I don't think you ever wanna return errors all the way up to hyper as that will terminate the connection without sending a response. So we recommended always using handle_error to convert errors into responses.」于是改成「always require all services to be infallible」,HandleErrorLayer 和 error_handling 模块也是这个 PR 加的。PR 里还写了另一层考虑:「Crucially this means Router's error type is now always Infallible since all the services it contains are infallible. In the future, once we box all routes, this means all services will have the same type and can be stored in a map.」今天 Router 把所有端点擦成同一个 Route 存进 HashMap<RouteId, Endpoint<S>>(Route 与类型擦除),前提就是这一步。0.3.0 的 CHANGELOG 记作「All services part of the router are now required to be infallible」和「Router::check_infallible has been removed since routers are always infallible with the error handling changes」。

修法

两条路:保留 tower 的 Timeout,在它外面用 HandleErrorLayer 把错误变成响应;或者换成 tower-http 的 TimeoutLayer,它超时时直接造一个响应,错误类型不变。下面把三种写法放在一起,handler 睡 1 秒,超时 50 毫秒:

axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["timeout", "util"] }
tower-http = { version = "=0.7.1", features = ["timeout"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "time", "test-util"] }
use std::time::Duration;

use axum::body::Body;
use axum::error_handling::HandleErrorLayer;
use axum::extract::Request;
use axum::http::StatusCode;
use axum::response::Response;
use axum::routing::get;
use axum::{BoxError, Router};
use tower::timeout::error::Elapsed;
use tower::{Service, ServiceBuilder, ServiceExt};

async fn slow() -> &'static str {
  tokio::time::sleep(Duration::from_secs(1)).await;
  "done"
}

async fn on_error(err: BoxError) -> (StatusCode, String) {
  if err.is::<Elapsed>() {
    (StatusCode::SERVICE_UNAVAILABLE, "timed out".to_owned())
  } else {
    (StatusCode::INTERNAL_SERVER_ERROR, format!("unhandled: {err}"))
  }
}

async fn show<S>(name: &str, svc: S)
where
  S: Service<Request, Response = Response> + Clone,
  S::Error: std::fmt::Debug,
{
  for uri in ["/slow"] {
    let res = svc.clone().oneshot(Request::get(uri).body(Body::empty()).unwrap()).await.unwrap();
    let status = res.status();
    let body = axum::body::to_bytes(res.into_body(), 1024).await.unwrap();
    println!("{name:<36} {uri} {status} {:?}", String::from_utf8_lossy(&body));
  }
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  let routes = || Router::new().route("/slow", get(slow));
  let ms50 = Duration::from_millis(50);

  // A: tower TimeoutLayer + HandleErrorLayer in one Router::layer call.
  let app = routes().layer(
    ServiceBuilder::new()
      .layer(HandleErrorLayer::new(on_error))
      .layer(tower::timeout::TimeoutLayer::new(ms50)),
  );
  show("A Router::layer(HandleError+Timeout)", app).await;

  // B: the same pair wrapped around the whole Router.
  let app = ServiceBuilder::new()
    .layer(HandleErrorLayer::new(on_error))
    .layer(tower::timeout::TimeoutLayer::new(ms50))
    .service(routes());
  show("B around the Router", app).await;

  // C: tower-http's TimeoutLayer answers with a response itself.
  let app = routes().layer(tower_http::timeout::TimeoutLayer::with_status_code(
    StatusCode::SERVICE_UNAVAILABLE,
    ms50,
  ));
  show("C tower_http TimeoutLayer", app).await;
}

start_paused = true 让 tokio 的时钟在没有任务可跑时直接跳到下一个计时器,结果和真实计时无关。M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:

A Router::layer(HandleError+Timeout) /slow 503 Service Unavailable "timed out"
B around the Router                  /slow 503 Service Unavailable "timed out"
C tower_http TimeoutLayer            /slow 503 Service Unavailable ""

A:同一次 .layer,HandleErrorLayer 在前。 ServiceBuilder 先写的在外层,套出来是 HandleError<Timeout<Route>>。HandleError 的 Error 是 Infallible,内层的 Err 交给 on_error,on_error 用 err.is::<Elapsed>() 分辨是不是超时。两个 layer 必须在同一次 .layer 里:分成两次调用,第一次 .layer(TimeoutLayer) 就过不了 Into<Infallible>。另一种写法是挂在 MethodRouter 上再 .handle_error(on_error),MethodRouter::layer 允许中间的错误类型,见 layer 与 route_layer。

B:包住整个 Router。 同样两层,用 ServiceBuilder::service(router) 包在外面。HandleError 在 Router 外面也能用:HandleErrorLayer 是普通的 tower Layer,产出的服务错误类型是 Infallible、响应类型是 axum 的 Response,正好满足 axum::serve 的约束;换掉上一节那个编译不过的程序里的 app,axum::serve(listener, ServiceExt::<Request>::into_make_service(app)) 就能编译、运行。区别在范围:A 只包已有的路由和 fallback,B 连路由本身也算进超时。

C:tower-http 的 TimeoutLayer。 它的 Service 实现 写的是 type Error = S::Error,内层是 Route 时就是 Infallible,直接 Router::layer 即可,不需要 HandleErrorLayer。超时响应是空 body,状态码由 with_status_code 指定(旧的 TimeoutLayer::new 固定 408,已 deprecated)。它和 tower 的 Timeout 在 poll 顺序上的差别,见 其余常用中间件。

需要 HandleErrorLayer超时响应适合
tower TimeoutLayer要,且在它外面、同一次 .layer你的函数决定,可以带 body、按错误类型分支和 LoadShed、Buffer 等一起用,统一处理 BoxError
tower-http TimeoutLayer不要指定状态码,空 body只想要一个超时

小结

  • 报错第一行只说 Box<dyn Error + Send + Sync> 转不成 Infallible,不提哪个中间件;看最后的 note 是不是指向 Router::layer 的 Into<Infallible>,再找错误类型是 BoxError 的 layer(Timeout、LoadShed、Buffer)。
  • HandleErrorLayer 必须在出错的 layer 外面:ServiceBuilder 里先写,且在同一次 .layer 里。放反了会多一个「HandleError<Route, ..>: Service<_> is not satisfied」。
  • 从外面包整个 Router 时,错误推迟到 axum::serve,变成 E0271 expected Infallible, found Box<dyn Error + Send + Sync>。
  • 约束的理由:错误到达 hyper 会直接关连接、不发响应。0.3.0(#402)起 axum 要求所有服务都不出错。
  • 只想要超时,用 tower-http 的 TimeoutLayer::with_status_code,它的错误类型就是内层的,不需要 HandleErrorLayer。

💬 关联讨论

发起讨论
加载中...