Rust in Everything
基于 axum 0.8.9

一个请求穿过 axum

主线位置:两条主线都在。本页先把一个小应用装配起来(装配主线),再给它发几个请求(请求主线),每一步都打印出来,看清哪些事只在启动时发生一次,哪些事每个请求都要再做一遍。细节留给后面的章节。
sequenceDiagram
  participant B as 启动代码
  participant R as Router
  participant M as MethodRouter
  participant F as from_fn 中间件
  participant H as HandlerService
  participant U as get_user
  Note over B,U: 装配主线:启动时一次
  B->>M: get(get_user) 放进 get 槽
  B->>R: route 两次,路径进 matchit
  B->>R: layer:现成的 Route 当场套
  B->>R: with_state:状态克隆 6 次
  R->>M: 方法槽变 Route,补套 layer
  Note over B,U: 请求主线:每个请求一次
  B->>R: oneshot GET /users/7
  R->>R: matchit 匹配,存路径参数
  R->>M: 按方法选 get 槽
  M->>F: 克隆 Route,oneshot
  F->>H: next.run(req)
  H->>U: 依次跑 State、Caller、Path
  U-->>H: String
  H-->>F: into_response
  F-->>B: 200 OK

程序

两个路由:GET /users/{id} 用到状态、一个自定义提取器和 Path,GET /health 什么都不用。整个 Router 外面套一个 from_fn 中间件。为了看清每件事在什么时候发生,程序做了四件事:

  • AppState 手写 Clone,每克隆一次计数加一。
  • 自定义提取器 Caller 实现 FromRequestParts,被调用时打印一行。
  • 用 LogLayer 包住 from_fn(log_mw),每次 Layer::layer 被调用(也就是中间件真正套到某个服务上)时计数并打印。
  • 中间件打印它看到的 MatchedPath。

不起服务器:Router<()> 是一个 tower Service,直接用 oneshot 调用(怎么用它写测试见 oneshot)。程序放在 src/bin/walk.rs,所以输出里的类型名带 walk:: 前缀。

axum = { version = "=0.8.9", default-features = false, features = ["matched-path"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use std::any::type_name_of_val;
use std::sync::atomic::{AtomicUsize, Ordering};

use axum::body::Body;
use axum::extract::{FromRequestParts, MatchedPath, Path, Request, State};
use axum::http::{request::Parts, StatusCode};
use axum::middleware::{from_fn, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use http_body_util::BodyExt;
use tower::{Layer, ServiceExt};

// 数 AppState 被 clone 了几次
static CLONES: AtomicUsize = AtomicUsize::new(0);
// 数 Layer::layer 被调用了几次
static LAYERS: AtomicUsize = AtomicUsize::new(0);

fn clones() -> usize {
  CLONES.load(Ordering::SeqCst)
}

struct AppState {
  name: &'static str,
}

impl Clone for AppState {
  fn clone(&self) -> Self {
    CLONES.fetch_add(1, Ordering::SeqCst);
    Self { name: self.name }
  }
}

// 自定义提取器:从 x-caller 头里取调用者
struct Caller(String);

impl<S: Send + Sync> FromRequestParts<S> for Caller {
  type Rejection = (StatusCode, &'static str);

  async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
    println!("      Caller::from_request_parts");
    let value = parts.headers.get("x-caller").and_then(|v| v.to_str().ok());
    match value {
      Some(v) => Ok(Caller(v.to_owned())),
      None => Err((StatusCode::BAD_REQUEST, "missing x-caller")),
    }
  }
}

// 包住 from_fn 的 Layer,只为了打印 layer() 什么时候被调用
#[derive(Clone)]
struct LogLayer<L>(L);

impl<L: Layer<I>, I> Layer<I> for LogLayer<L> {
  type Service = L::Service;

  fn layer(&self, inner: I) -> Self::Service {
    let n = LAYERS.fetch_add(1, Ordering::SeqCst) + 1;
    println!("      Layer::layer #{n}");
    self.0.layer(inner)
  }
}

async fn log_mw(req: Request, next: Next) -> Response {
  let matched = req.extensions().get::<MatchedPath>().map(|m| m.as_str().to_owned());
  println!("    middleware before: {} {} matched={:?}", req.method(), req.uri(), matched);
  let res = next.run(req).await;
  println!("    middleware after:  {}", res.status());
  res
}

async fn get_user(State(state): State<AppState>, caller: Caller, Path(id): Path<u32>) -> String {
  println!("      handler get_user(id={id})");
  format!("user {id} on {}, asked by {}", state.name, caller.0)
}

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

async fn send(app: &Router, method: &str, uri: &str) {
  let before = clones();
  println!("\n[request] {method} {uri}");
  let req = Request::builder()
    .method(method)
    .uri(uri)
    .header("x-caller", "alice")
    .body(Body::empty())
    .unwrap();
  let res = app.clone().oneshot(req).await.unwrap();
  let status = res.status();
  let allow = res.headers().get("allow").map(|v| v.to_str().unwrap().to_owned());
  let body = res.into_body().collect().await.unwrap().to_bytes();
  println!("  -> {status} allow={allow:?} body={body:?}");
  println!("  AppState clones during this request: {}", clones() - before);
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  println!("[build] start");
  let state = AppState { name: "demo" };

  let users = get(get_user);
  println!("[build] get(get_user)      : {}", type_name_of_val(&users));

  let app = Router::new()
    .route("/users/{id}", users)
    .route("/health", get(health));
  println!("[build] .route x2          : {}", type_name_of_val(&app));

  println!("[build] .layer(...)");
  let app = app.layer(LogLayer(from_fn(log_mw)));

  println!("[build] .with_state(...)");
  let app: Router = app.with_state(state);
  println!("[build] after with_state   : {}", type_name_of_val(&app));
  println!(
    "[build] done: Layer::layer calls = {}, AppState clones = {}",
    LAYERS.load(Ordering::SeqCst),
    clones()
  );

  let layers_before = LAYERS.load(Ordering::SeqCst);
  send(&app, "GET", "/users/7").await;
  send(&app, "GET", "/users/abc").await;
  send(&app, "GET", "/health").await;
  send(&app, "POST", "/health").await;
  send(&app, "GET", "/nope").await;
  println!(
    "\n[after 5 requests] Layer::layer calls = {}",
    LAYERS.load(Ordering::SeqCst) - layers_before
  );
}

输出

M2 Max、rustc 1.99.0,跑了 3 次,输出逐字节相同(没有任何依赖时序的部分):

[build] start
[build] get(get_user)      : axum::routing::method_routing::MethodRouter<walk::AppState>
[build] .route x2          : axum::routing::Router<walk::AppState>
[build] .layer(...)
      Layer::layer #1
      Layer::layer #2
      Layer::layer #3
      Layer::layer #4
      Layer::layer #5
[build] .with_state(...)
      Layer::layer #6
      Layer::layer #7
[build] after with_state   : axum::routing::Router
[build] done: Layer::layer calls = 7, AppState clones = 6

[request] GET /users/7
    middleware before: GET /users/7 matched=Some("/users/{id}")
      Caller::from_request_parts
      handler get_user(id=7)
    middleware after:  200 OK
  -> 200 OK allow=None body=b"user 7 on demo, asked by alice"
  AppState clones during this request: 5

[request] GET /users/abc
    middleware before: GET /users/abc matched=Some("/users/{id}")
      Caller::from_request_parts
    middleware after:  400 Bad Request
  -> 400 Bad Request allow=None body=b"Invalid URL: Cannot parse `abc` to a `u32`"
  AppState clones during this request: 5

[request] GET /health
    middleware before: GET /health matched=Some("/health")
      handler health
    middleware after:  200 OK
  -> 200 OK allow=None body=b"ok"
  AppState clones during this request: 4

[request] POST /health
    middleware before: POST /health matched=Some("/health")
    middleware after:  405 Method Not Allowed
  -> 405 Method Not Allowed allow=Some("GET,HEAD") body=b""
  AppState clones during this request: 0

[request] GET /nope
    middleware before: GET /nope matched=None
    middleware after:  404 Not Found
  -> 404 Not Found allow=None body=b""
  AppState clones during this request: 0

[after 5 requests] Layer::layer calls = 0

前半段是装配主线,后半段是请求主线。下面逐段对照源码。

装配:类型在变

三行 type_name_of_val 是装配主线的骨架:

步骤类型
get(get_user)MethodRouter<AppState>
.route(...) 两次Router<AppState>
.with_state(state)Router,即 Router<()>(默认类型参数不打印)

MethodRouter<AppState> 和 Router<AppState> 里的 AppState 不是「装着一个 AppState」,而是「还欠一个 AppState」:Router 的文档原话是「Router<S> means a router that is missing a state of type S」。欠着状态的路由器不能处理请求:Service<Request<B>> 只为 Router<()> 实现,忘了 with_state 是编译错误,不是运行时错误。这一点在 状态:State 与 FromRef 展开。

get(get_user) 走到 MethodRouter::on,把 handler 包成 BoxedIntoRoute 放进 get 槽:

        self.on_endpoint(
            filter,
            MethodEndpoint::BoxedHandler(BoxedIntoRoute::from_handler(handler)),
        )

BoxedIntoRoute::from_handler 存下的是 handler 和一个函数指针 |handler, state| Route::new(Handler::with_state(handler, state)):「拿到状态之后怎么变成 Route」。这时它还不是服务。

Router::route 由 PathRouter::route 完成:分配一个 RouteId,把「路径 → RouteId」插进 matchit,把「RouteId → Endpoint::MethodRouter」放进 HashMap。同一路径第二次 route 会和已有的 MethodRouter 合并,所以 .route("/", get(a)).route("/", post(b)) 能用。

.layer 为什么套了 5 次,with_state 又补了 2 次

Router::layer 把同一个 Layer 分别交给三处:

        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)),
        })

Router::new() 时这三处已经各有东西,加上两条路由,一共 7 个服务要套:

位置是什么什么时候套次数
两个 MethodRouter 的方法级 fallbackMethodRouter::new 放的 Route,回 405.layer 时,当场2
fallback_routernew_fallback 在 / 和 /{*__private__axum_fallback} 各注册一个 NotFound.layer 时,当场2
catch_all_fallbackFallback::Default(Route::new(NotFound)).layer 时,当场1
两个 get 槽BoxedIntoRoute,还欠状态with_state 时2

已经是 Route 的,Route::layer 当场调用 layer.layer(self),再装箱成新的 Route。还欠状态的 BoxedIntoRoute 套不上去:from_fn 的服务要包住一个真正的服务,而 handler 没有状态就造不出 HandlerService。于是 BoxedIntoRoute::map 只把「怎么套」这个闭包存进 Map,等 into_route 拿到状态、先造出 Route,再把闭包作用上去:

    fn into_route(self: Box<Self>, state: S) -> Route<E2> {
        (self.layer)(self.inner.into_route(state))
    }

这就是输出里 #6、#7 出现在 .with_state(...) 之后的原因。之后发了 5 个请求,Layer::layer 一次也没再调用:服务树在启动时已经造完了。

❗IMPORTANT
动脑筋:没有状态的应用不调用 with_state,layer 什么时候套?

平台 M2 Max、rustc 1.99.0。下面的应用没有状态,类型本来就是 Router<()>,不调用 with_state 也能直接 oneshot。LogLayer 和上面的一样,只计数不打印。先不调用 with_state 发 2 个请求,再调用 with_state(()) 发 2 个请求。两次各打印多少?

axum = { version = "=0.8.9", default-features = false }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use std::sync::atomic::{AtomicUsize, Ordering};

use axum::body::Body;
use axum::extract::Request;
use axum::middleware::{from_fn, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use tower::{Layer, ServiceExt};

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

#[derive(Clone)]
struct LogLayer<L>(L);

impl<L: Layer<I>, I> Layer<I> for LogLayer<L> {
  type Service = L::Service;

  fn layer(&self, inner: I) -> Self::Service {
    LAYERS.fetch_add(1, Ordering::SeqCst);
    self.0.layer(inner)
  }
}

async fn mw(req: Request, next: Next) -> Response {
  next.run(req).await
}

async fn get_twice(app: &Router, label: &str) {
  let before = LAYERS.load(Ordering::SeqCst);
  for _ in 0..2 {
    let req = Request::get("/").body(Body::empty()).unwrap();
    app.clone().oneshot(req).await.unwrap();
  }
  println!("{label}: Layer::layer calls during 2 requests = {}", LAYERS.load(Ordering::SeqCst) - before);
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  // 没有状态的应用:类型本来就是 Router<()>,不调用 with_state 也能直接用
  let app: Router = Router::new()
    .route("/", get(|| async { "hi" }))
    .layer(LogLayer(from_fn(mw)));
  println!("after build: Layer::layer calls = {}", LAYERS.load(Ordering::SeqCst));

  get_twice(&app, "without with_state").await;

  let app = app.with_state(());
  get_twice(&app, "after with_state(())").await;
}

with_state 本身做的事在 Router::with_state → PathRouter::with_state → MethodRouter::with_state → MethodEndpoint::with_state 这四层里,每层都按值往下传一份状态。输出里的 6 次克隆就是这么来的:

位置克隆
Router::with_state:给 path_router、fallback_router 各一份,最后一份移进 catch_all_fallback2
PathRouter::with_state:两条路由的 Endpoint::MethodRouter 各一份2
MethodEndpoint::with_state:两个 get 槽 into_route(state.clone())2

注意 /health 的 handler 根本不用状态,它的 HandlerService 里照样存了一份 AppState:with_state 不看 handler 用不用,Router<S> 里所有 handler 的 S 都是同一个。Router 那一层的 Endpoint::MethodRouter 并没有变成 Route,变的是 MethodRouter 里面的方法槽:MethodEndpoint::BoxedHandler 变成 MethodEndpoint::Route。

请求:值在流动

路由在中间件之前

第一个请求的第一行输出是中间件打印的,而它已经看到了 matched=Some("/users/{id}")。MatchedPath 是 PathRouter::call_with_state 在 matchit 匹配成功后插进 extensions 的,所以中间件执行时路由已经做完了。原因在上一节:Router::layer 不是给整个 Router 包一层,而是给每个路由的 Route 各包一层。请求先由 PathRouter 匹配路径、由 MethodRouter 选出方法槽,然后才进入这个槽里被包好的 Route,中间件在它的最外层。想要在路由之前就执行的中间件(比如改写路径),要把整个 Router 当成服务再包一层,见 layer 与 route_layer。

PathRouter 匹配上以后,还把 matchit 抓到的 id=7 存成 UrlParams 放进 extensions(insert_url_params),后面的 Path 提取器从这里读。

提取器按参数顺序执行

get_user 的参数是 State、Caller、Path。impl_handler! 生成的 call 按参数顺序展开:

            fn call(self, req: Request, state: S) -> Self::Future {
                let (mut parts, body) = req.into_parts();
                Box::pin(async move {
                    $(
                        let $ty = match $ty::from_request_parts(&mut parts, &state).await {
                            Ok(value) => value,
                            Err(rejection) => return rejection.into_response(),
                        };
                    )*

                    let req = Request::from_parts(parts, body);

                    let $last = match $last::from_request(req, &state).await {
                        Ok(value) => value,
                        Err(rejection) => return rejection.into_response(),
                    };

                    self($($ty,)* $last,).await.into_response()
                })
            }

前两个参数走 from_request_parts,最后一个 Path 走 from_request(Path 只实现了 FromRequestParts,axum-core 的 blanket 实现把它也变成 FromRequest,见 FromRequestParts 与 FromRequest)。/users/abc 那次,Caller 成功了,Path<u32> 解析失败,rejection 直接 into_response() 成 400,handler get_user 那一行没有出现:你的函数根本没被调用。中间件照样看到了这个 400,因为 rejection 也是一个普通的响应。

405 和 404 也经过中间件

POST /health:路径匹配上了,MethodRouter 的 post 槽是空的,落到方法级 fallback,回 405,Allow: GET,HEAD(注册 get 时一起记下了 HEAD)。GET /nope:path_router 没匹配上,交给 fallback_router,回 404;fallback 路由不设置 MatchedPath,所以是 None。两个请求都打印了 middleware before,因为上表里这些 fallback 在 .layer 时都被套过。换成 route_layer 再跑,404 那次不再经过中间件,405 那次仍然经过:route_layer 只跳过 fallback_router 和 catch_all_fallback,每个 MethodRouter 连同它的方法级 fallback 照样被套上。区别见 layer 与 route_layer。

每个请求克隆 5 次状态

AppState clones during this request: 5,五次分别发生在:

#位置克隆的是什么
1MethodRouter::call_with_state:route.clone().oneshot_inner_owned(req)整个 Route,一路克隆到最里面的 HandlerService
2FromFn::call:self.inner.clone()中间件里面的 Route
3next.run(req) 调用里面的 Route,Route::oneshot_inner:self.0.clone()又一份 HandlerService
4HandlerService::call:self.state.clone()交给 Handler::call 的那份状态
5State 的 from_request_parts 调 FromRef,对 T: Clone 的 blanket 实现就是 clone()你的 handler 拿到的那份

/health 没有 State 参数,少第 5 次,是 4。405、404 的路由里没有 HandlerService,是 0。去掉中间件再跑同一个程序,/users/7 是 3 次、/health 是 2 次:每多一层 from_fn,多 2 次(第 2、3 行)。

所以 crate 文档说「State is cloned for every request. Wrapping your state in Arc makes those clones cheap.」实际的次数取决于中间件的层数,不是 1。状态里放 Arc,或者每个字段本身都是 Arc / Copy,克隆就只是几次引用计数加一。

放进 axum::serve 之后

上面一直用 oneshot,真正跑服务时请求从 axum::serve 进来。handle_connection 每接受一条连接,先调用一次 make_service.call(IncomingStream { .. }) 拿到这条连接的服务,用 TowerToHyperService 包好交给 hyper-util 的 auto::Builder(后半段见 hyper 卷)。直接传 Router 时,这个 call 是 Router 对 IncomingStream 的 Service 实现:

        fn call(&mut self, _req: serve::IncomingStream<'_, L>) -> Self::Future {
            // call `Router::with_state` such that everything is turned into `Route` eagerly
            // rather than doing that per request
            std::future::ready(Ok(self.clone().with_state(())))
        }

每条连接一次 with_state(())。如果交进来的 Router 里还有 BoxedIntoRoute(无状态的应用、从没调用过 with_state),layer 就在这里每条连接套一次。下面用真实的监听端口验证,客户端是 hyper 自己的 client::conn,开两条连接,每条发 3 个请求:

axum = { version = "=0.8.9", default-features = false, features = ["tokio", "http1"] }
tower = { version = "=0.5.3", features = ["util"] }
hyper = { version = "=1.12.0", features = ["client", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "net"] }

LogLayer、mw 和动脑筋里的一样,main 换成:

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let app: Router = Router::new()
    .route("/", get(|| async { "hi" }))
    .layer(LogLayer(from_fn(mw)));
  println!("after build: {}", layers());

  let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });

  for conn_no in 1..=2 {
    let before = layers();
    let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
    let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await.unwrap();
    tokio::spawn(conn);
    for _ in 0..3 {
      let req = Request::get("/").header("host", "x").body(Body::empty()).unwrap();
      let res = sender.send_request(req).await.unwrap();
      assert_eq!(res.status(), 200);
    }
    println!("connection {conn_no}, 3 requests: Layer::layer calls = {}", layers() - before);
  }
}

(layers() 读 LAYERS 计数,TokioIo 来自 hyper_util::rt。)跑了 3 次,输出相同:

after build: 4
connection 1, 3 requests: Layer::layer calls = 1
connection 2, 3 requests: Layer::layer calls = 1

每条连接套一次,同一条连接上的 3 个请求共用。把 let app = app.into_make_service(); 放在 serve 之前再跑,into_make_service 在转换时就调用了一次 with_state(()),之后每条连接只是克隆现成的服务树(3 次输出相同):

after into_make_service: 5
connection 1, 3 requests: Layer::layer calls = 0
connection 2, 3 requests: Layer::layer calls = 0

有状态的应用不受影响:with_state(state) 之后已经没有 BoxedIntoRoute,每条连接的 with_state(()) 只是把 Route 原样搬一遍。层里如果有「每个实例各自一份」的东西(比如 tower ConcurrencyLimit 的信号量),套的次数就决定了它们的作用范围。

📝NOTE
冷知识:axum::serve 曾经每个请求都重新套一遍 layer

上面那段 call 里的 with_state(()) 是 #2586(「Fix layers being cloned for each request」)加的,在那之前这里只是 std::future::ready(Ok(self.clone()))。直接 axum::serve(listener, router) 的无状态应用,路由器里一直留着 BoxedIntoRoute,每个请求在 MethodRouter::call_with_state 里现场 into_route,layer 也就每个请求套一遍,正是动脑筋里「without with_state」那一行的情形。

issue #2582 发现的后果是:MethodRouter::layer 文档里 get(handler).layer(ConcurrencyLimitLayer::new(64)) 的例子「compiles, but no concurrency limit is enforced. This is because each request to GET / is sent through a different ConcurrencyLimit instance, each with its own semaphore.」PR 的说明是:「Turns out we were missing a .with_state(()) to eagerly apply layers like we used to do for .into_make_service(). This might also have a performance impact.」CHANGELOG 记为「Fixed layers being cloned when calling axum::serve directly with a Router or MethodRouter」,随 axum 0.7.5(2024-03-24)发布。into_make_service 里那段一模一样的注释,就是 PR 说的「like we used to do」。

动脑筋答案

after build: Layer::layer calls = 4
without with_state: Layer::layer calls during 2 requests = 2
after with_state(()): Layer::layer calls during 2 requests = 0

跑了 3 次,输出相同。逐行看:

  • after build: 4:一个 MethodRouter 的方法级 fallback 1 次,fallback_router 的两个 NotFound 2 次,catch_all_fallback 1 次。唯一的 handler 还是 BoxedIntoRoute,它的那次被推迟了。
  • without with_state: 2:每个请求 1 次。Router<()> 的 call 把 () 当状态传进 MethodRouter::call_with_state,get 槽还是 MethodEndpoint::BoxedHandler,于是走 handler.clone().into_route(state),现场造 Route、现场套 layer,用完就扔。类型上 Router<()> 和 with_state(()) 之后完全一样,区别只在里面的方法槽有没有被提前变成 Route。
  • after with_state(()): 0:with_state(()) 把方法槽变成了 MethodEndpoint::Route,layer 在那一刻套好,之后每个请求只是克隆。

所以「装配主线在启动时做完」有一个前提:with_state 被调用过。有状态的应用一定会调用它;无状态的应用,axum::serve 和 into_make_service 会替你调用。自己拿 Router<()> 去 oneshot、或者交给别的服务器时,先 .with_state(()) 一下。

小结

  • 装配主线里类型在变:get 得到 MethodRouter<S>,route 得到 Router<S>,with_state 得到 Router<()>;S 表示「还欠一个 S」。
  • handler 在 with_state 之前是 BoxedIntoRoute(handler 加一个「拿到状态后怎么变成 Route」的函数指针);.layer 对它只记下闭包,拿到状态时再套。已经是 Route 的(各种 fallback)当场套。
  • with_state 逐层按值传状态,本例启动时克隆 6 次;不用状态的 handler 也存一份。
  • Router::layer 包的是每个路由的 Route,所以中间件在路由之后、提取器之前执行,能看到 MatchedPath;405、404 的 fallback 也被包了。
  • 提取器按参数顺序执行,失败时 rejection 直接变成响应,你的函数不会被调用。
  • 本例每个请求克隆状态 5 次:Route 克隆 1 次、每层 from_fn 2 次、HandlerService::call 1 次、State 提取 1 次。状态要放 Arc。
  • axum::serve 每条连接调用一次 with_state(())(#2586);没有 with_state 过的 Router<()> 直接 oneshot,layer 会每个请求重套一遍。

💬 关联讨论

发起讨论
加载中...