Rust in Everything
基于 axum 0.8.9

状态:State 与 FromRef

主线位置:两条主线都有。装配主线的最后一步是 with_state:它把所有「欠着状态」的 handler 连同一份状态装成 Route,Router<AppState> 变成 Router<()>,这时才能交给 axum::serve。请求主线上,State<T> 是一个普通的 FromRequestParts 提取器,从 handler 手里那份状态用 FromRef 取出一份 T。

flowchart TB
  subgraph build["启动时:装配"]
    h["handler:State AppState"] --> mr["get:MethodRouter AppState"]
    mr --> r1["Router AppState:欠状态"]
    r1 --> ws["with_state(state)"]
    ws --> route["端点各带一份 state"]
    route --> r0["Router ():可 serve"]
  end
  subgraph perreq["每个请求"]
    c1["Route clone:第 1 次"] --> c2["call:第 2 次"]
    c2 --> c3["FromRef:第 3 次"]
  end
  r0 --> c1

State 本身只有几行。这一页的重点是它背后的类型参数:Router<S> 里的 S 在装配时怎么出现、怎么消失,以及状态在每个请求里被复制了几次。

Router<S>:S 是还欠着的状态

Router 的文档第一句就说明了 S 的含义:

/// The router type for composing handlers and services.
///
/// `Router<S>` means a router that is _missing_ a state of type `S` to be able
/// to handle requests. Thus, only `Router<()>` (i.e. without missing state) can
/// be passed to [`serve`]. See [`Router::with_state`] for more details.
///
/// [`serve`]: crate::serve()
#[must_use]
pub struct Router<S = ()> {
    inner: Arc<RouterInner<S>>,
}

S 不是「路由器持有的状态」,而是「路由器还缺的状态」。它从 handler 的参数一路推出来。State 的提取器实现只对满足 FromRef 的外层状态成立:

impl<OuterState, InnerState> FromRequestParts<OuterState> for State<InnerState>
where
    InnerState: FromRef<OuterState>,
    OuterState: Send + Sync,
{
    type Rejection = Infallible;

    async fn from_request_parts(
        _parts: &mut Parts,
        state: &OuterState,
    ) -> Result<Self, Self::Rejection> {
        let inner_state = InnerState::from_ref(state);
        Ok(Self(inner_state))
    }
}

async fn handler(State(s): State<AppState>) 要成为 Handler<T, S>,每个参数都得实现 FromRequestParts<S>(Handler trait 一章讲宏展开)。于是 get(handler) 推出 MethodRouter<AppState>,Router::route 要求 MethodRouter<S> 和自己的 S 一致,整个 Router 也就成了 Router<AppState>。

只有 Router<()> 实现了 tower Service,见 impl Service<Request<B>> for Router<()>:

impl<B> Service<Request<B>> for Router<()>
where
    B: HttpBody<Data = bytes::Bytes> + Send + 'static,
    B::Error: Into<axum_core::BoxError>,
{
    // ...
    fn call(&mut self, req: Request<B>) -> Self::Future {
        let req = req.map(Body::new);
        self.call_with_state(req, ())
    }
}

axum::serve 要的 Service<IncomingStream> 也只给了 Router<()>。所以忘了 with_state 不会等到运行时才发现,编译就过不去:

axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["full"] }
use axum::{Router, extract::State, routing::get};

#[derive(Clone)]
struct AppState;

async fn handler(State(_s): State<AppState>) -> &'static str { "ok" }

#[tokio::main]
async fn main() {
  let app = Router::new().route("/", get(handler));
  let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
  axum::serve(listener, app).await.unwrap();
}

rustc 1.99.0 的第一条错误(后面还有一条 Serve<...> is not a future,是同一个原因):

error[E0277]: the trait bound `for<'a> Router<AppState>: tower_service::Service<IncomingStream<'a, tokio::net::TcpListener>>` is not satisfied
   --> src/bin/missing_state.rs:12:25
    |
 12 |   axum::serve(listener, app).await.unwrap();
    |   -----------           ^^^ the trait `for<'a> tower_service::Service<IncomingStream<'a, tokio::net::TcpListener>>` is not implemented for `Router<AppState>`
    |   |
    |   required by a bound introduced by this call
    |
help: `Router` implements trait `tower_service::Service<Request>`
   --> axum-0.8.9/src/routing/mod.rs:549:5
    |
549 | /     impl<L> Service<serve::IncomingStream<'_, L>> for Router<()>
550 | |     where
551 | |         L: serve::Listener,
    | |___________________________^ `tower_service::Service<IncomingStream<'_, L>>`
...
569 | / impl<B> Service<Request<B>> for Router<()>
570 | | where
571 | |     B: HttpBody<Data = bytes::Bytes> + Send + 'static,
572 | |     B::Error: Into<axum_core::BoxError>,
    | |________________________________________^ `tower_service::Service<axum::http::Request<B>>`

报错里的关键是 Router<AppState>:类型参数直接写出了缺的是什么。如果路由在一个返回 Router(即 Router<()>)的函数里组装,错误更直白:

error[E0308]: mismatched types
  --> src/bin/missing_state2.rs:13:3
   |
12 | fn app() -> Router {
   |             ------ expected `Router` because of return type
13 |   api()
   |   ^^^^^ expected `Router`, found `Router<AppState>`
   |
   = note: expected struct `Router<()>`
              found struct `Router<AppState>`

给错了状态类型也一样在编译期暴露。handler 要 State<Db>、路由器却 with_state(AppState),而 Db 没有实现 FromRef<AppState>:

async fn handler(State(_db): State<Db>) -> &'static str { "ok" }

fn main() {
  let _app: Router = Router::new()
    .route("/", get(handler))
    .with_state(AppState);
}
error[E0308]: mismatched types
   --> src/bin/wrong_state.rs:14:17
    |
 14 |     .with_state(AppState);
    |      ---------- ^^^^^^^^ expected `Db`, found `AppState`
    |      |
    |      arguments to this method are incorrect
    |
note: method defined here
   --> axum-0.8.9/src/routing/mod.rs:408:12
    |
408 |     pub fn with_state<S2>(self, state: S) -> Router<S2> {
    |            ^^^^^^^^^^

FromRef 只有 T: Clone 时 FromRef<T> for T 这一个实现可用,类型推断就把 S 定成了 Db,错误落在 with_state 的参数上。

with_state:把状态装进每个端点

Router::with_state 把状态分发给三处:

    pub fn with_state<S2>(self, state: S) -> Router<S2> {
        map_inner!(self, this => RouterInner {
            path_router: this.path_router.with_state(state.clone()),
            fallback_router: this.fallback_router.with_state(state.clone()),
            default_fallback: this.default_fallback,
            catch_all_fallback: this.catch_all_fallback.with_state(state),
        })
    }

PathRouter::with_state 给每个路由的 MethodRouter 一份 state.clone(),MethodRouter::with_state 再给每个方法槽一份。真正干活的是 MethodEndpoint::with_state:

    fn with_state<S2>(self, state: &S) -> MethodEndpoint<S2, E> {
        match self {
            MethodEndpoint::None => MethodEndpoint::None,
            MethodEndpoint::Route(route) => MethodEndpoint::Route(route),
            MethodEndpoint::BoxedHandler(handler) => {
                MethodEndpoint::Route(handler.into_route(state.clone()))
            }
        }
    }

装配时,get(handler) 并不知道状态,只能先把 handler 擦成 BoxedIntoRoute<S, E>:一个「给我一份 S,我就变成 Route」的盒子。盒子里的转换函数在 BoxedIntoRoute::from_handler:

        Self(Box::new(MakeErasedHandler {
            handler,
            into_route: |handler, state| Route::new(Handler::with_state(handler, state)),
        }))

Handler::with_state 返回 HandlerService,就是 handler 和 state 两个字段;Route::new 再把它装进 BoxCloneSyncService(tower 卷 装箱)。with_state 之后,BoxedHandler 全部变成了 Route,S 从类型里消失。Route 的结构和装箱的代价在 route 与装箱 一章。

返回类型是 Router<S2> 而不是 Router<()>:S2 由调用方决定。这让状态不同的子路由可以先各自填好状态,再 nest 进去:

#[derive(Clone)]
struct AppState { name: &'static str }

#[derive(Clone)]
struct ApiState { version: u32 }

async fn root(State(s): State<AppState>) -> String { format!("app = {}", s.name) }
async fn api(State(s): State<ApiState>) -> String { format!("api v{}", s.version) }

#[tokio::main]
async fn main() {
  // Router<ApiState> --with_state--> Router<AppState> (S2 inferred from .nest)
  let api_router = Router::new()
    .route("/info", get(api))
    .with_state(ApiState { version: 2 });

  let app: Router = Router::new()
    .route("/", get(root))
    .nest("/api", api_router)
    .with_state(AppState { name: "demo" });

  for uri in ["/", "/api/info"] {
    let res = app.clone().oneshot(Request::get(uri).body(Body::empty()).unwrap()).await.unwrap();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("{uri:<10} {}", String::from_utf8_lossy(&body));
  }
}
/          app = demo
/api/info  api v2

api_router 的 with_state 返回值被 nest 推成 Router<AppState>:它已经不缺 ApiState 了,但要和外层一起「欠一个 AppState」。nest 要求两边 S 相同(pub fn nest(self, path: &str, router: Router<S>)),这正是 S2 的用处。

📝NOTE
冷知识:with_state 返回 Router<S2>,是 0.6.0 正式版前最后一刻改的

0.6.0 的预发布版里,with_state 先是构造函数(Router::with_state(state).route(...)),rc.5 改成在末尾调用、产出一个单独的 RouterService 类型(#1532)。正式版前一天合并的 #1552 又改了一次:签名变成 pub fn with_state<S2>(self, state: S) -> Router<S2, B>,PR 的说法是「you get back a new Router and you get decide what state it needs」,由此可以 nest、merge 状态不同的路由器;同时「Router<()> now implements Service and RouterService has been removed」。PR 也写明了代价:没调用过 with_state 的路由器被直接 call 时,没有机会提前把端点转成 Route,「That means we need to do it for each request」。首次发布于 axum-v0.6.0。

这个代价今天还在。MethodRouter::call_with_state 碰到还没转换的端点,每个请求现做一次:

                        MethodEndpoint::BoxedHandler(handler) => {
                            let route = handler.clone().into_route(state);
                            return route.oneshot_inner_owned($req);
                        }

所以 with_state.md 建议不需要状态的路由器也调一次 .with_state(())。into_make_service 自己会调;直接 axum::serve(listener, router) 时,Service<IncomingStream> 的 call 每条连接执行一次 self.clone().with_state(()),也不会落到每请求转换。只有拿 Router<()> 直接当 Service 调用(例如测试里 oneshot)才会走这条慢路径。

FromRef:从总状态取子状态

State 只认一个状态类型,handler 想只拿其中一部分,靠 FromRef:

// NOTE: This trait is defined in axum-core, even though it is mainly used with `State` which is
// defined in axum. That allows crate authors to use it when implementing extractors.
pub trait FromRef<T> {
    /// Converts to this type from a reference to the input type.
    fn from_ref(input: &T) -> Self;
}

impl<T> FromRef<T> for T
where
    T: Clone,
{
    fn from_ref(input: &T) -> Self {
        input.clone()
    }
}

两点:

  1. 输入是引用,输出是值。State<AppState> 走的是下面那个 blanket 实现,就是一次 clone。
  2. trait 放在 axum-core 而不是 axum,注释说明了原因:写提取器的库只依赖 axum-core 就能要求「用户的状态里能取出我的状态」(MyLibraryState: FromRef<S>),state.rs 的文档「For library authors」一节给了这个写法。axum-core 为什么单独成 crate 见 源码地图。

#[derive(FromRef)](要开 macros feature)为结构体的每个字段各生成一个实现。生成代码在 axum-macros 的 expand_field:

    let body = if let Some(field_ident) = &field.ident {
        if matches!(field_ty, Type::Reference(_)) {
            quote_spanned! {span=> state.#field_ident }
        } else {
            quote_spanned! {span=> state.#field_ident.clone() }
        }
    } else {
        // ...
        quote_spanned! {span=> state.#idx.clone() }
    };

    quote_spanned! {span=>
        #[allow(clippy::clone_on_copy, clippy::clone_on_ref_ptr)]
        impl ::axum::extract::FromRef<#state> for #field_ty {
            fn from_ref(state: &#state) -> Self {
                #body
            }
        }
    }

每个字段一句 state.field.clone()。由此有几条限制:结构体不能带泛型(expand 一开头就报 `#[derive(FromRef)]` doesn't support generics);两个字段类型相同会生成两个冲突的 impl,要给其中一个加 #[from_ref(skip)];字段类型必须是 Clone。

📝NOTE
没有蠢问题

问: 既然 from_ref 拿到的是 &AppState,为什么不能直接写 State<&Db>,省掉一次 clone?

答: 提取器的返回值和状态的借用没有任何生命周期关联:FromRequestParts::from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, ...> 里的 Self 不能借用 state。原因在 handler 的调用方式上:Handler::call(self, req, state: S) 按值收下状态,把它移进一个 Pin<Box<dyn Future + Send>>,提取器拿到的 &state 只在这个 future 内部有效,而 Self 要能被移来移去、交给你的函数。想要便宜的「共享」,就让 clone 本身便宜:把大的字段包进 Arc。

每个请求 clone 几次

lib.rs 的「Sharing state with handlers」一节写的是「State is cloned for every request. Wrapping your state in Arc makes those clones cheap.」。到底几次,可以数出来:给状态写一个会计数的 Clone。

axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["full"] }
tower = { version = "=0.5.3", features = ["util"] }
use std::sync::atomic::{AtomicUsize, Ordering};
use axum::{Router, body::Body, extract::State, http::Request, routing::get};
use tower::ServiceExt;

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

struct AppState;

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

async fn with_state(State(_s): State<AppState>) -> &'static str { "ok" }
async fn without_state() -> &'static str { "ok" }

fn report(label: &str) {
  println!("{label:<28} clones = {}", CLONES.swap(0, Ordering::SeqCst));
}

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/a", get(with_state))
    .route("/b", get(without_state).post(without_state))
    .with_state(AppState);
  report("with_state (2 routes)");

  for path in ["/a", "/a", "/b"] {
    let req = Request::get(path).body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    report(&format!("GET {path} -> {}", res.status()));
  }
}

app.clone() 只是 Arc 计数加一(Router 的 Clone 是 Arc::clone(&self.inner)),不碰状态。

❗IMPORTANT
动脑筋:with_state 那一行打印几次?GET /a 一次请求里 AppState::clone 被调用几次?GET /b 的 handler 根本不要状态,又是几次?

M2 Max、rustc 1.99.0、--release。答案在本页最后。

State 与 Extension

在 0.6 之前,共享状态的标准做法是 Extension:用 .layer(Extension(value)) 把值放进每个请求的 http::Extensions,handler 再按类型取出来。两边的代码:

// the layer: AddExtension::call
    fn call(&mut self, mut req: Request<ResBody>) -> Self::Future {
        req.extensions_mut().insert(self.value.clone());
        self.inner.call(req)
    }

// the extractor
    fn from_extensions(extensions: &Extensions) -> Option<Self> {
        extensions.get::<T>().cloned().map(Extension)
    }

(AddExtension::call、Extension::from_extensions。)Extensions 是一个以 TypeId 为键的 map,类型对不上、或者这个请求根本没经过那层 layer,都要到运行时取的那一刻才知道,结果是 MissingExtension,500。下面的 /after 在 .layer 之后才加,不在 layer 里(layer 只包已有的路由,见 layer 与 route_layer),照样编译通过:

use axum::{Extension, Router, body::Body, http::Request, routing::get};
use http_body_util::BodyExt;
use tower::ServiceExt;

#[derive(Clone)]
struct Db;

async fn handler(Extension(_db): Extension<Db>) -> &'static str { "ok" }

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/before", get(handler))
    .layer(Extension(Db))
    // added after .layer(): not wrapped by it
    .route("/after", get(handler));

  for uri in ["/before", "/after"] {
    let req = Request::get(uri).body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    let status = res.status();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("{uri:<8} {} {}", status.as_u16(), String::from_utf8_lossy(&body));
  }
}
/before  200 ok
/after   500 Missing request extension: Extension of type `extension::Db` was not found. Perhaps you forgot to add it? See `axum::Extension`.

(依赖再加 http-body-util = "0.1"。)

State<T>Extension<T>
值放在哪每个 Route 里的 HandlerService.state每个请求的 Extensions,由 layer 每次插入
怎么找到FromRef,编译期确定按 TypeId 查 map,运行时
缺了会怎样编译错误(上一节)500 Missing request extension
能不能每个请求不同不能,with_state 时就定了能,中间件可以按请求插入

最后一行是 Extension 留下来的理由。State 的文档原话:「State is global and used in every request a router with state receives. For accessing data derived from requests, such as authorization data, see [Extension].」认证中间件解析出的用户、请求 ID 这类每个请求不同的东西,仍然走 extensions;ConnectInfo 也是这样实现的,见 Path、Query 与请求元数据。

📝NOTE
冷知识:State 是 0.6 才有的,此前只有 Extension

#1155(2022-08-17 合并,首次发布于 axum-v0.6.0-rc.1,正式版 axum-v0.6.0)引入了 State。PR 描述:「Attempting to extract states that haven't seen set results in compile errors, rather than runtime errors.」做法是给 MethodRouter、FromRequest、Handler 都加上状态类型参数:「All extractors just requires some generic state S but State fixes that to be some specific type.」这就是今天 FromRequestParts<S> 那个 S 的来历。CHANGELOG 里给出的迁移建议是:「We recommend migrating from Extension to State for sharing application state since that is more type safe and faster.」FromRef trait 也是这个 PR 加的(rc.1 的 CHANGELOG 已经用 impl FromRef<AppState> for HttpClient 演示子状态),#[derive(FromRef)] 晚一些,是 #1430,首次进入 axum-v0.6.0-rc.3。

动脑筋答案

连跑 3 次,输出相同:

with_state (2 routes)        clones = 7
GET /a -> 200 OK             clones = 3
GET /a -> 200 OK             clones = 3
GET /b -> 200 OK             clones = 2

装配时 7 次。 Router::with_state 给 path_router 和 fallback_router 各一份(2);PathRouter::with_state 给两个路由的 MethodRouter 各一份(2);MethodEndpoint::with_state 给每个非空方法槽一份:/a 的 GET,/b 的 GET 和 POST(3)。最后那份被移进 catch_all_fallback,默认 fallback 不需要状态,没有 clone。

GET /a 每次 3 次,分别在:

  1. MethodRouter::call_with_state 里的 route.clone().oneshot_inner_owned($req)。Route 是 BoxCloneSyncService,克隆它就是克隆里面的 HandlerService,而 HandlerService 的 Clone 是 state: self.state.clone()。
  2. HandlerService::call:Handler::call(handler, req, self.state.clone()),因为 Handler::call 按值收状态。
  3. State<AppState> 提取时 InnerState::from_ref(state),走 FromRef<T> for T 的 input.clone()。

GET /b 2 次:前两次和 handler 要不要状态无关,第三次只在有 State 参数时发生。

再看子状态。AppState 里放一个 Db,#[derive(FromRef)],handler 只要 State<Db>,两个类型各自计数(AppState 的 Clone 会顺带 clone 一次 db):

axum = { version = "=0.8.9", features = ["macros"] }
#[derive(FromRef)]
struct AppState {
  db: Db,
  #[from_ref(skip)]
  _secret: String,
}
impl Clone for AppState {
  fn clone(&self) -> Self {
    APP.fetch_add(1, SeqCst);
    AppState { db: self.db.clone(), _secret: self._secret.clone() }
  }
}

async fn handler(State(_db): State<Db>) {}

一次请求(连跑 3 次相同):

one request: AppState::clone = 2, Db::clone = 3

State<Db> 并没有让整个 AppState 少 clone:前两次照样复制整个 AppState(连带复制两次 Db),第三次是生成的 from_ref 里的 state.db.clone()。子状态省掉的只是 handler 拿到的那一份。状态里如果有 String、Vec、HashMap 这类深拷贝的字段,每个请求就要复制两到三遍,这就是文档建议把状态包进 Arc(或让每个字段本身就是 Arc、连接池句柄这类廉价 clone 的类型)的原因。

小结

  • Router<S> 的 S 是「还欠着的状态」,从 handler 的 State<T> 参数经 FromRef 推出来。只有 Router<()> 实现 Service,忘了 with_state 是编译错误。
  • with_state 把每个 BoxedHandler 连同一份状态装成 Route(HandlerService { handler, state }),返回 Router<S2>,S2 由调用方定,所以子路由可以先填自己的状态再 nest。
  • 没调用 with_state 就直接当 Service 用,每个请求都要现做 into_route;into_make_service 和 axum::serve 会替你提前做。
  • FromRef 在 axum-core,from_ref(&T) -> Self;derive 为每个字段生成 state.field.clone()。
  • 带 State 的请求实测 clone 状态 3 次,不带的 2 次;状态要么本身廉价可 clone,要么包进 Arc。
  • Extension 运行时按类型查找,缺了是 500;适合每个请求不同的数据,应用级状态用 State。

💬 关联讨论

发起讨论
加载中...