Rust in Everything
基于 axum 0.8.9

layer 与 route_layer

主线位置:两条主线都在。装配主线上,.layer(...) 在调用的那一刻把 tower Layer 套到路由器里已有的每个服务上;请求主线上,这些中间件在路由匹配之后、提取器之前执行。route_layer 和 layer 的区别只在装配时少套了哪几个服务,请求时的结果却差在状态码上。

flowchart TB
  req["请求"] --> pr{"PathRouter 匹配路径"}
  pr -->|命中| mr{"MethodRouter 选方法"}
  pr -->|没命中| fb["fallback:404"]
  mr -->|有这个方法| slot["方法槽 Route"]
  mr -->|没有| mf["方法级 fallback:405"]
  slot -.- a1["layer、两种 route_layer 都套"]
  mf -.- a2["layer、Router::route_layer 套"]
  fb -.- a3["只有 layer 套"]

Service、Layer 本身和 ServiceBuilder 的顺序规则在 tower 卷讲过(Layer 与 Stack、层的顺序反了)。这一页只讲 axum 自己的部分:Router、MethodRouter、Handler 三处的 layer 各套在哪、什么时候套、对错误类型有什么要求。

Router::layer:套在已有的每个服务上

Router::layer 不包整个路由器,而是把同一个 layer 分给 RouterInner 的三个字段:

    pub fn layer<L>(self, layer: L) -> Router<S>
    where
        L: Layer<Route> + Clone + Send + Sync + 'static,
        L::Service: Service<Request> + Clone + Send + Sync + 'static,
        <L::Service as Service<Request>>::Response: IntoResponse + 'static,
        <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
        <L::Service as Service<Request>>::Future: Send + 'static,
    {
        map_inner!(self, this => RouterInner {
            path_router: this.path_router.layer(layer.clone()),
            fallback_router: this.fallback_router.layer(layer.clone()),
            default_fallback: this.default_fallback,
            catch_all_fallback: this.catch_all_fallback.map(|route| route.layer(layer)),
        })
    }

PathRouter::layer 遍历 self.routes 这个 HashMap<RouteId, Endpoint<S>>,对每个端点调用 Endpoint::layer:是 MethodRouter 就交给 MethodRouter::layer,是 Route(route_service 注册的)就交给 Route::layer。layer 不记在 Router 上(只有还欠状态的 handler 槽会把它存进闭包,等 with_state 时再套),所以之后再 .route(...) 加的路由不会有它。fallback_router 是 Router::new() 时就注册好的两条 NotFound 路由,catch_all_fallback 是兜底的 NotFound,两者都被套上,所以 404 也经过中间件。这几处各套几次、with_state 之前的 handler 为什么要推迟到 with_state 才套,见 一个请求穿过 axum;RouterInner 各字段的含义见 Router 的内部结构。

下面的程序在 .layer 之后再加一条 /late,中间件打印它见到的请求和 MatchedPath:

axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::{MatchedPath, Request};
use axum::middleware::{from_fn, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use tower::ServiceExt;

// Logs every request it sees, then passes it on.
async fn log(req: Request, next: Next) -> Response {
  let matched = req.extensions().get::<MatchedPath>().map(|m| m.as_str().to_owned());
  println!("    log sees {} {} matched={matched:?}", req.method(), req.uri());
  next.run(req).await
}

async fn send(app: &Router, method: &str, uri: &str) {
  let req = Request::builder().method(method).uri(uri).body(Body::empty()).unwrap();
  let res = app.clone().oneshot(req).await.unwrap();
  println!("  {:<10} -> {}", format!("{method} {uri}"), res.status());
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let app = Router::new()
    .route("/a", get(|| async { "a" }))
    .layer(from_fn(log))
    // added after .layer(): not wrapped
    .route("/late", get(|| async { "late" }));

  for (m, u) in [("GET", "/a"), ("GET", "/late"), ("POST", "/a"), ("GET", "/nope")] {
    send(&app, m, u).await;
  }
}

M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:

    log sees GET /a matched=Some("/a")
  GET /a     -> 200 OK
  GET /late  -> 200 OK
    log sees POST /a matched=Some("/a")
  POST /a    -> 405 Method Not Allowed
    log sees GET /nope matched=None
  GET /nope  -> 404 Not Found
  • /late 返回 200,但中间件没打印:它在 .layer 之后才加进来。
  • POST /a 的 405 和 GET /nope 的 404 都经过了中间件。405 来自 /a 这个 MethodRouter 的方法级 fallback(MethodRouter),404 来自 fallback_router。
  • 中间件看到 /a 时 MatchedPath 已经有值:路由在中间件之前做完了。404 的那次是 None。

route_layer:少套哪两处

Router::route_layer 和 layer 的约束完全一样,区别在函数体:

        map_inner!(self, this => RouterInner {
            path_router: this.path_router.route_layer(layer),
            fallback_router: this.fallback_router,
            default_fallback: this.default_fallback,
            catch_all_fallback: this.catch_all_fallback,
        })

fallback_router 和 catch_all_fallback 原样保留,没匹配到任何路径的请求不经过中间件。PathRouter::route_layer 先检查有没有路由,没有就 panic:「Adding a route_layer before any routes is a no-op. Add the routes you want the layer to apply to first.」然后和 PathRouter::layer 一样,对每个端点调 endpoint.layer(...)。

关键在这一步:端点是 MethodRouter 时,调用的是 MethodRouter::layer,不是 MethodRouter::route_layer。MethodRouter::layer 套的是全部方法槽加上方法级 fallback:

        let layer_fn = move |route: Route<E>| route.layer(layer.clone());

        MethodRouter {
            get: self.get.map(layer_fn.clone()),
            head: self.head.map(layer_fn.clone()),
            // ...
            connect: self.connect.map(layer_fn.clone()),
            fallback: self.fallback.map(layer_fn),
            allow_header: self.allow_header,
        }

而 MethodRouter::route_layer 只套九个方法槽,fallback 不动:

        let layer_fn = move |svc| Route::new(layer.layer(svc));

        self.get = self.get.map(layer_fn.clone());
        self.head = self.head.map(layer_fn.clone());
        // ...
        self.connect = self.connect.map(layer_fn);

        self

所以两个 route_layer 管的范围不一样:Router::route_layer 跳过的是「路径没匹配上」,MethodRouter::route_layer 跳过的是「路径匹配上了但方法不对」。两份文档的说法也对应:Router 的那份说它防止把「404 Not Found 变成 401 Unauthorized」,MethodRouter 的那份说的是「405 Method Not Allowed 变成 401 Unauthorized」。

📝NOTE
冷知识:route_layer 最早叫 layer_on_matching_route

Router::route_layer 是 #474 加的,随 axum 0.3.2(tag v0.3.2)发布。PR 描述了当时的问题:用 .layer(RequireAuthorizationLayer::bearer("password")) 保护 /foo 之后,「calling some unknown route like GET /not-found would also return 401 Unauthorized」,原因是「the 404 you'd otherwise see is generated by a fallback service stored on Router. When adding a layer to the router the layer would also be applied to the fallback」。作者认为大多数中间件就该对所有请求生效(「you don't wanna skip logging for unmatched requests」),所以没改 layer,而是另加一个方法,PR 里起的名字是 layer_on_matching_route,合并时叫 route_layer。今天 Router::route_layer 的函数体就是这个思路:fallback_router 和 catch_all_fallback 不套。

下面用一个没有 token 头就返回 401 的中间件,比较四种挂法。每种挂法发三个请求:匹配的 GET /a、方法不对的 POST /a、路径不存在的 GET /nope,都不带 token:

axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::{from_fn, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use tower::ServiceExt;

// Rejects every request without a `token` header.
async fn auth(req: Request, next: Next) -> Response {
  if req.headers().contains_key("token") {
    next.run(req).await
  } else {
    StatusCode::UNAUTHORIZED.into_response()
  }
}

async fn row(name: &str, app: Router) {
  let mut cells = Vec::new();
  for (method, uri) in [("GET", "/a"), ("POST", "/a"), ("GET", "/nope")] {
    let req = Request::builder().method(method).uri(uri).body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    cells.push(format!("{method} {uri} {}", res.status().as_u16()));
  }
  println!("{name:<26} {}", cells.join(" | "));
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let h = || async { "a" };

  row("Router::layer", Router::new().route("/a", get(h)).layer(from_fn(auth))).await;
  row("Router::route_layer", Router::new().route("/a", get(h)).route_layer(from_fn(auth))).await;
  row(
    "MethodRouter::route_layer",
    Router::new().route("/a", get(h).route_layer(from_fn(auth))),
  )
  .await;
  row(
    "fallback + route_layer",
    Router::new()
      .route("/a", get(h))
      .fallback(|| async { (StatusCode::IM_A_TEAPOT, "fallback") })
      .route_layer(from_fn(auth)),
  )
  .await;
}
❗IMPORTANT
动脑筋:Router::route_layer 那一行,POST /a 的客户端看到 401 还是 405?M2 Max、rustc 1.99.0。答案在本页最后。

程序里最后一种挂法(先 .fallback(...) 再 .route_layer(...))是照着文档写的。0.8.9 里 Router::route_layer 的文档(docs/routing/route_layer.md)仍写着「first add your routes (and / or fallback) and then call route_layer」,读起来像是 fallback 也会被套。issue #3421 指出了这一点,随后的 #3567(axum 0.8.8,CHANGELOG 记作「Clarify documentation for Router::route_layer」)删掉了「(and / or fallback)」,但改动的文件是 docs/method_routing/route_layer.md,Router 那份没动。以函数体为准:Router::route_layer 不碰 fallback_router 和 catch_all_fallback。

三个层级与执行顺序

layer 有三个挂点:Handler::layer 只包一个 handler,MethodRouter::layer 包一条路径的所有方法(连同 405),Router::layer 包所有已有路由和 fallback。同一层级上多次 .layer,后写的在外层;三个层级之间,Router 在最外,Handler 在最里。下面的程序用同一个中间件函数,靠 from_fn_with_state 传进来的名字区分各层(from_fn_with_state 见 from_fn 与它的兄弟),最后两段留给下一节:

axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::{MatchedPath, Request, State};
use axum::handler::Handler;
use axum::middleware::{from_fn_with_state, map_request, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use tower::{Layer, ServiceExt};

// One middleware function, named by its state.
async fn log(State(name): State<&'static str>, req: Request, next: Next) -> Response {
  let matched = req.extensions().get::<MatchedPath>().is_some();
  println!("  {name} sees request (matched={matched})");
  let res = next.run(req).await;
  println!("  {name} sees response");
  res
}

async fn handler() -> &'static str {
  println!("  handler");
  "ok"
}

fn req(uri: &str) -> Request {
  Request::get(uri).body(Body::empty()).unwrap()
}

fn rewrite(mut req: Request) -> Request {
  if req.uri() == "/old" {
    *req.uri_mut() = "/".parse().unwrap();
  }
  req
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  println!(".layer(A).layer(B).layer(C)");
  let app = Router::new()
    .route("/", get(handler))
    .layer(from_fn_with_state("A", log))
    .layer(from_fn_with_state("B", log))
    .layer(from_fn_with_state("C", log));
  app.oneshot(req("/")).await.unwrap();

  println!("Handler::layer + MethodRouter::layer + Router::layer");
  let app = Router::new()
    .route(
      "/",
      get(handler.layer(from_fn_with_state("handler-level", log)))
        .layer(from_fn_with_state("method-level", log)),
    )
    .layer(from_fn_with_state("router-level", log));
  app.oneshot(req("/")).await.unwrap();

  println!("whole Router wrapped: outer.layer(router)");
  let app = Router::new()
    .route("/", get(handler))
    .layer(from_fn_with_state("inner", log));
  let wrapped = from_fn_with_state("outer", log).layer(app);
  wrapped.oneshot(req("/")).await.unwrap();

  println!("rewrite /old -> / with Router::layer");
  let app = Router::new().route("/", get(handler)).layer(map_request(rewrite_async));
  let res = app.oneshot(req("/old")).await.unwrap();
  println!("  status {}", res.status());

  println!("rewrite /old -> / around the whole Router");
  let app = Router::new().route("/", get(handler));
  let res = tower::util::MapRequestLayer::new(rewrite).layer(app).oneshot(req("/old")).await.unwrap();
  println!("  status {}", res.status());
}

async fn rewrite_async(req: Request) -> Request {
  rewrite(req)
}

M2 Max、rustc 1.99.0,连跑 3 次输出相同。前两段:

.layer(A).layer(B).layer(C)
  C sees request (matched=true)
  B sees request (matched=true)
  A sees request (matched=true)
  handler
  A sees response
  B sees response
  C sees response
Handler::layer + MethodRouter::layer + Router::layer
  router-level sees request (matched=true)
  method-level sees request (matched=true)
  handler-level sees request (matched=true)
  handler
  handler-level sees response
  method-level sees response
  router-level sees response

原因都在 Route::layer:Route::new((MapErrLayer::new(Into::into), layer).layer(self)),新 layer 包住到目前为止的整个 Route。tower 卷的 层的顺序反了 拿这一点和 ServiceBuilder(先写的在外)做了对比,那里也有用 ServiceBuilder 把一组 layer 一次交给 Router::layer 的写法。

三个层级还有一处不同:Layer::layer 在什么时候被调用。

挂法Layer::layer 何时调用中间件的错误类型
Router::layer调用 .layer 时;欠状态的 handler 推迟到 with_state要 Into<Infallible>
MethodRouter::layer同上任意 NewError,得到 MethodRouter<S, NewError>
Handler::layer每个请求一次要 Infallible

Handler::layer 返回的 Layered 只存下 layer 和 handler,套的动作在它的 Handler::call 里:

    fn call(self, req: Request, state: S) -> Self::Future {
        use futures_util::future::{FutureExt, Map};

        let svc = self.handler.with_state(state);
        let svc = self.layer.layer(svc);
        // ...
        > = svc.oneshot(req).map(|result| match result {
            Ok(res) => res.into_response(),
            Err(err) => match err {},
        });

Handler::call 每个请求调用一次,于是 layer 每个请求套一遍,套出来的服务用完就扔。对无状态的 layer 没有区别;对 ConcurrencyLimitLayer 这种在 layer 时新建信号量的,每个请求拿到的都是一个新信号量。用一个只计数的 Counted 包住 ConcurrencyLimitLayer::new(1),三种挂法各同时发 3 个请求,handler 睡 100ms:

axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["limit", "util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "time"] }
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};

use axum::body::Body;
use axum::extract::Request;
use axum::handler::Handler;
use axum::routing::get;
use axum::Router;
use tower::limit::ConcurrencyLimitLayer;
use tower::{Layer, ServiceExt};

static LAYERS: AtomicUsize = AtomicUsize::new(0);

// Counts Layer::layer calls, delegates to the wrapped layer.
#[derive(Clone)]
struct Counted<L>(L);

impl<L: Layer<I>, I> Layer<I> for Counted<L> {
  type Service = L::Service;
  fn layer(&self, inner: I) -> Self::Service {
    LAYERS.fetch_add(1, Ordering::SeqCst);
    self.0.layer(inner)
  }
}

async fn slow() {
  tokio::time::sleep(Duration::from_millis(100)).await;
}

async fn run(name: &str, app: Router) {
  let app = app.with_state(());
  let before = LAYERS.load(Ordering::SeqCst);
  let start = Instant::now();
  let tasks: Vec<_> = (0..3)
    .map(|_| {
      let app = app.clone();
      tokio::spawn(async move {
        app.oneshot(Request::get("/").body(Body::empty()).unwrap()).await.unwrap();
        start.elapsed().as_millis() / 10 * 10
      })
    })
    .collect();
  let mut done = Vec::new();
  for t in tasks {
    done.push(t.await.unwrap());
  }
  done.sort();
  println!(
    "{name:<20} Layer::layer during 3 requests = {}, done at ~{done:?} ms",
    LAYERS.load(Ordering::SeqCst) - before
  );
}

#[tokio::main]
async fn main() {
  let limit = || Counted(ConcurrencyLimitLayer::new(1));
  run("Handler::layer", Router::new().route("/", get(slow.layer(limit())))).await;
  run("MethodRouter::layer", Router::new().route("/", get(slow).layer(limit()))).await;
  run("Router::layer", Router::new().route("/", get(slow)).layer(limit())).await;
}

连跑 3 次输出相同(毫秒数向下取整到 10,完成时间排了序):

Handler::layer       Layer::layer during 3 requests = 3, done at ~[100, 100, 100] ms
MethodRouter::layer  Layer::layer during 3 requests = 0, done at ~[100, 200, 300] ms
Router::layer        Layer::layer during 3 requests = 0, done at ~[100, 200, 300] ms

Handler::layer 那一行:3 个请求调了 3 次 Layer::layer,上限 1 没起作用,三个请求同时完成。另外两行在 with_state(()) 时就套好了,请求时 0 次,三个请求排队。Handler::layer 自己的文档例子正是 handler.layer(ConcurrencyLimitLayer::new(64));要限流就挂在 MethodRouter 或 Router 上,或者用 GlobalConcurrencyLimitLayer(信号量在 Arc 里,套几遍都是同一个,见 ConcurrencyLimit)。

📝NOTE
冷知识:Handler::layer 在 0.6 之前只套一次

0.5 的 Handler::layer 是 Layered::new(layer.layer(self.into_service())):调用 layer 的当下就把 handler 变成服务、套上 layer,之后每个请求复用。#1155(引入 State,首次发布于 axum-v0.6.0-rc.1,正式版 0.6.0)给 handler 加上了状态类型参数,PR 的说法是「MethodRouter, FromRequest, RequestParts, and Handler then all have a new type parameter for the state」。从此 handler 要拿到状态才能变成服务,而状态只在 Handler::call(self, req, state) 里才有,这个 PR 于是把 Layered 改成存下 layer,在 call 里先 self.handler.with_state(state) 再 self.layer.layer(svc),就是上面引用的那几行。Router、MethodRouter 直接交给 axum::serve 时每个请求重套 layer 的问题,0.7.5 由 #2586 修掉了(见 一个请求穿过 axum 的冷知识);Handler::layer 的套法在 Layered::call 里,到 0.8.9 还是每个请求一次。

路由之前执行:把整个 Router 包起来

中间件在路由之后执行,所以它改请求的 URI 不会影响路由结果。axum 文档 middleware.md 的「Rewriting request URI in middleware」 给的办法是反过来:Router<()> 本身是 Service,用 Layer::layer(router) 把整个路由器包住。上一节程序的后两段就是这个:日志中间件分别放在里外两层,同一个改写函数(把 /old 改成 /)用两种方式挂上。输出:

whole Router wrapped: outer.layer(router)
  outer sees request (matched=false)
  inner sees request (matched=true)
  handler
  inner sees response
  outer sees response
rewrite /old -> / with Router::layer
  status 404 Not Found
rewrite /old -> / around the whole Router
  handler
  status 200 OK

包在外面的 outer 先于路由执行,MatchedPath 还没有;Router::layer 挂上的 inner 在路由之后。map_request 挂在 Router::layer 上时,路由已经按 /old 匹配失败,进入的是 404 fallback,改 URI 已经晚了。

代价是包完之后的类型不再是 Router,不能再 .route、.with_state;交给 axum::serve 时,文档的例子用 axum::ServiceExt::into_make_service 把它变成 make service。

错误类型:Into<Infallible> 与 HandleErrorLayer

Router::layer 的约束里有一条 Error: Into<Infallible>:中间件不能产生错误,只能产生响应。axum 的 middleware.md 解释了原因:「If hyper receives an error the connection will be closed without sending a response. Thus axum requires those errors to be handled gracefully」。不少 tower 中间件会产生错误,比如 tower 的 TimeoutLayer,超时时返回 Err(Elapsed),错误类型是 BoxError(Timeout)。直接挂上去:

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

use axum::{Router, routing::get};
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/timeout_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`

报错里没有出现 Timeout,只说 Box<dyn Error + Send + Sync> 转不成 Infallible,指向 Router::layer 的第 308 行。看到 Into<Infallible> 就是这个原因。

约束写成 Into<Infallible> 而不是 = Infallible,Route::layer 里的 MapErrLayer::new(Into::into) 再把错误转成 Infallible。这是 issue #922 要求的:一个组合两个服务的中间件,用枚举 MyError<Infallible, Infallible> 表示错误,它同样不可能有值,却不是 Infallible 这个类型。issue 的原话是「axum really just cares that the error isn't possible rather than needing it to be the specific type Infallible」。改动在 axum 0.6.0 发布(#948)。

修法是在 TimeoutLayer 外面套一层 HandleErrorLayer,它的 Service 实现 把内层的 Err 交给你的函数变成响应,自己的 Error 是 Infallible:

    type Response = Response;
    type Error = Infallible;
    type Future = future::HandleErrorFuture;

    fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: Request<B>) -> Self::Future {
        let f = self.f.clone();

        let clone = self.inner.clone();
        let inner = std::mem::replace(&mut self.inner, clone);

        let future = Box::pin(async move {
            match inner.oneshot(req).await {
                Ok(res) => Ok(res.into_response()),
                Err(err) => Ok(f(err).await.into_response()),
            }
        });

        future::HandleErrorFuture { future }
    }

poll_ready 永远就绪,等内层就绪的事放进了 oneshot。这个函数前面还可以放最多 16 个 FromRequestParts<()> 提取器(impl_service! 从 T1 展开到 T1..T16),错误放最后。两种挂法:

use std::time::Duration;

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

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

// Extractors (FromRequestParts<()>) may come before the error.
async fn on_error(method: Method, uri: Uri, err: BoxError) -> (StatusCode, String) {
  if err.is::<Elapsed>() {
    (StatusCode::REQUEST_TIMEOUT, format!("{method} {uri} timed out"))
  } else {
    (StatusCode::INTERNAL_SERVER_ERROR, format!("unhandled: {err}"))
  }
}

async fn show(name: &str, app: Router) {
  let res = app.oneshot(Request::get("/slow").body(Body::empty()).unwrap()).await.unwrap();
  let status = res.status();
  let body = axum::body::to_bytes(res.into_body(), usize::MAX).await.unwrap();
  println!("{name}: {status} {body:?}");
}

#[tokio::main]
async fn main() {
  // One Router::layer call, HandleErrorLayer outside TimeoutLayer.
  let app = Router::new().route("/slow", get(slow)).layer(
    ServiceBuilder::new()
      .layer(HandleErrorLayer::new(on_error))
      .layer(TimeoutLayer::new(Duration::from_millis(50))),
  );
  show("ServiceBuilder", app).await;

  // MethodRouter::layer may change the error type; handle_error brings it back.
  let app = Router::new().route(
    "/slow",
    get(slow)
      .layer(TimeoutLayer::new(Duration::from_millis(50)))
      .handle_error(on_error),
  );
  show("MethodRouter ", app).await;
}

依赖同上,连跑 3 次输出相同:

ServiceBuilder: 408 Request Timeout b"GET /slow timed out"
MethodRouter : 408 Request Timeout b"GET /slow timed out"
  • 在 Router 上,HandleErrorLayer 和 TimeoutLayer 必须放进同一次 .layer 调用,ServiceBuilder 里先写 HandleErrorLayer(外层)。拆成 .layer(TimeoutLayer::new(..)).layer(HandleErrorLayer::new(..)) 两次调用,第一次就因为同样的 Into<Infallible> 编译不过;Router 里每个服务都是 Route(Route<Infallible>),没有地方暂存一个带错误的中间状态。
  • MethodRouter 可以:MethodRouter::layer 的约束是 Error: Into<NewError>,TimeoutLayer 之后类型变成 MethodRouter<S, BoxError>,再用 handle_error(就是 self.layer(HandleErrorLayer::new(f)))变回 MethodRouter<S, Infallible>,Router::route 才肯收。

tower-http 的 TimeoutLayer 不需要这一步:它超时时直接返回一个响应,错误类型不变,见 tower-http 的其余常用中间件。

动脑筋答案

连跑 3 次输出相同:

Router::layer              GET /a 401 | POST /a 401 | GET /nope 401
Router::route_layer        GET /a 401 | POST /a 401 | GET /nope 404
MethodRouter::route_layer  GET /a 401 | POST /a 405 | GET /nope 404
fallback + route_layer     GET /a 401 | POST /a 401 | GET /nope 418
  • Router::route_layer 下 POST /a 是 401。 PathRouter::route_layer 对 /a 的 MethodRouter 调的是 MethodRouter::layer,方法级 fallback 也被套上了,鉴权中间件先拒绝,405 永远出不来。客户端看到的是「没登录」,而不是「这个路径不支持 POST」。
  • GET /nope 是 404:fallback_router 和 catch_all_fallback 没套,鉴权中间件根本没被调用。这正是 route_layer 想解决的问题。
  • MethodRouter::route_layer 三格各不相同:只套方法槽,405 和 404 都原样返回。想让 401 只出现在「路径和方法都对」的请求上,要用这一种。
  • 自定义 fallback 加 Router::route_layer,GET /nope 是 418:Router::fallback 设置的 fallback 在 fallback_router / catch_all_fallback 里,route_layer 不套它,和调用顺序无关。要让 fallback 也经过鉴权,只能用 layer。

小结

  • Router::layer 把 layer 交给每个已有路由的端点、fallback_router 和 catch_all_fallback,layer 本身不保存,之后加的路由不受影响。
  • 中间件在路由之后执行,能看到 MatchedPath,改 URI 不影响路由;要在路由之前执行,用 Layer::layer(router) 包住整个 Router。
  • Router::route_layer 只跳过两个 fallback(404),每条路径的方法级 fallback 仍被套上,鉴权中间件会把 405 变成 401;MethodRouter::route_layer 才连 405 一起跳过。
  • 后 .layer 的在外层;三个层级 Router 在外、MethodRouter 居中、Handler 在内。
  • Handler::layer 在每个请求的 Handler::call 里重新 layer 一次,ConcurrencyLimitLayer 挂在这里不起作用。
  • Router::layer 要求中间件 Error: Into<Infallible>;产生错误的 tower 中间件要和 HandleErrorLayer 放进同一次 .layer,或者先挂在 MethodRouter 上再 handle_error。

💬 关联讨论

发起讨论
加载中...