Rust in Everything
基于 axum 0.8.9

from_fn 与它的兄弟

主线位置:两条主线都在。装配主线上,from_fn(f) 只是一个存着 f 的 Layer,Router::layer 把它套到每个 Route 外面(layer 与 route_layer)。请求主线上,FromFn::call 先像 handler 一样跑提取器,再把内层服务装箱成 Next,交给你的函数。

sequenceDiagram
  participant R as 外层 Route
  participant F as FromFn
  participant X as 提取器
  participant U as 你的函数
  participant N as Next
  participant I as 内层服务
  R->>F: call(req)
  F->>F: 换出就绪的 inner
  F->>X: 依次提取,失败即返回
  F->>N: 装箱 inner
  F->>U: f(提取结果, req, next)
  U->>N: next.run(req)
  N->>I: call(req)
  I-->>U: Response
  U-->>R: into_response

axum::middleware 里有四个把函数或类型变成中间件的入口:from_fn、map_request、map_response、from_extractor,各有一个 _with_state 版本。前三个的源码结构几乎一样:一个存着函数和状态的 Layer,一个按参数个数用宏展开的 Service 实现(from_fn、map_request 用 all_the_tuples! 展开 1–16 个,map_response 手写了 0–16 个提取器的 17 行 impl_service!)。from_extractor 没有函数,只有一个实现。这一页以 from_fn 为主,其余三个只讲不同的地方。

FromFn 是什么

from_fn 就是状态为 () 的 from_fn_with_state:

pub fn from_fn<F, T>(f: F) -> FromFnLayer<F, (), T> {
    from_fn_with_state((), f)
}

FromFnLayer 存 f、state 和一个 PhantomData<fn() -> T>,T 是参数类型的元组,作用和 Handler trait 里的标记类型一样:同一个函数类型对不同参数列表有不同的 Service 实现。Layer::layer 把它们连同内层服务放进 FromFn。from_fn 本身是 0.4.6 从 axum-extra 搬进来的(#719,CHANGELOG:「This previously lived in axum-extra but has been moved to axum」)。

FromFn::call:提取器、Next、你的函数

impl_service! 宏为 Next 之前 1–16 个参数的情形各生成一个 Service<Request> 实现。约束和 call:

        impl<F, Fut, Out, S, I, $($ty,)* $last> Service<Request> for FromFn<F, S, I, ($($ty,)* $last,)>
        where
            F: FnMut($($ty,)* $last, Next) -> Fut + Clone + Send + 'static,
            $( $ty: FromRequestParts<S> + Send, )*
            $last: FromRequest<S> + Send,
            Fut: Future<Output = Out> + Send + 'static,
            Out: IntoResponse + 'static,
            I: Service<Request, Error = Infallible>
            // ...
            fn call(&mut self, req: Request) -> Self::Future {
                let not_ready_inner = self.inner.clone();
                let ready_inner = std::mem::replace(&mut self.inner, not_ready_inner);

                let mut f = self.f.clone();
                let state = self.state.clone();
                let (mut parts, body) = req.into_parts();

                let future = 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(),
                    };

                    let inner = BoxCloneSyncService::new(MapIntoResponse::new(ready_inner));
                    let next = Next { inner };

                    f($($ty,)* $last, next).await.into_response()
                });

逐段看:

  1. mem::replace 换出就绪的那个 inner。 poll_ready 问的是 self.inner,call 把这个已就绪的实例拿走,留一个克隆在原处。这是 tower 里在 call 中克隆内层的标准写法,原因见 tower 卷 poll_ready must be called first。
  2. 提取器和 handler 一模一样。 函数的参数是 (T1, ..., Tn, last, Next):前面的要 FromRequestParts<S>,Next 前面那一个要 FromRequest<S>,通常就是 Request。这段展开和 impl_handler! 生成的 call(Handler trait)逐行相同,提取失败就把 rejection 变成响应返回,你的函数和内层服务都不会被调用。
  3. Next 是每个请求现做的。 就绪的 inner 包进 MapIntoResponse,再 BoxCloneSyncService::new 装箱。I 是这一层包住的服务:在 Router::layer 下是一个 Route,在 ServiceBuilder 里可能是下一层中间件。
  4. 整个过程是一个 Box::pin 的 future。 FromFn 的 Future 类型是 ResponseFuture,里面就是这个 BoxFuture,Error 是 Infallible。

下面的中间件在 Request 前面放了 Query<Lang> 和 HeaderMap:

axum = "=0.8.9"
serde = { version = "1", features = ["derive"] }
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::{FromRequestParts, Query, Request, State};
use axum::http::{request::Parts, HeaderMap, HeaderValue, StatusCode};
use axum::middleware::{from_extractor, from_fn, from_fn_with_state, map_request, map_response, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use serde::Deserialize;
use tower::ServiceExt;

#[derive(Deserialize)]
struct Lang {
  lang: String,
}

// Extractors first, then Request, then Next.
async fn with_extractors(Query(q): Query<Lang>, headers: HeaderMap, req: Request, next: Next) -> Response {
  println!("    mw: lang={} ua={:?}", q.lang, headers.get("user-agent"));
  next.run(req).await
}

#[derive(Clone)]
struct Limits {
  max_len: usize,
}

// State given to from_fn_with_state, independent of the Router's state.
async fn check_len(State(limits): State<Limits>, req: Request, next: Next) -> Result<Response, StatusCode> {
  if req.uri().path().len() > limits.max_len {
    return Err(StatusCode::URI_TOO_LONG);
  }
  Ok(next.run(req).await)
}

// map_request: Ok(req) goes on, Err(e) becomes the response.
async fn require_token(req: Request) -> Result<Request, StatusCode> {
  if req.headers().contains_key("token") { Ok(req) } else { Err(StatusCode::UNAUTHORIZED) }
}

async fn add_header(mut res: Response) -> Response {
  res.headers_mut().insert("x-mw", HeaderValue::from_static("map_response"));
  res
}

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

struct Auth;

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

  async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
    AUTH_RUNS.fetch_add(1, Ordering::SeqCst);
    match parts.headers.get("token") {
      Some(v) if v == "secret" => Ok(Auth),
      _ => Err((StatusCode::UNAUTHORIZED, "bad token")),
    }
  }
}

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

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

async fn send(app: &Router, uri: &str, token: Option<&str>) {
  let mut b = Request::get(uri).header("user-agent", "demo");
  if let Some(t) = token {
    b = b.header("token", t);
  }
  let res = app.clone().oneshot(b.body(Body::empty()).unwrap()).await.unwrap();
  let status = res.status();
  let x = res.headers().get("x-mw").cloned();
  let body = axum::body::to_bytes(res.into_body(), usize::MAX).await.unwrap();
  println!("  {uri} token={token:?} -> {} x-mw={x:?} body={body:?}", status.as_u16());
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  println!("[from_fn with Query + HeaderMap]");
  let app = Router::new().route("/", get(handler)).layer(from_fn(with_extractors));
  send(&app, "/?lang=zh", None).await;
  send(&app, "/", None).await;

  println!("[from_fn_with_state]");
  let app = Router::new()
    .route("/{*rest}", get(handler))
    .layer(from_fn_with_state(Limits { max_len: 8 }, check_len));
  send(&app, "/short", None).await;
  send(&app, "/much-too-long", None).await;

  println!("[map_request + map_response]");
  let app = Router::new()
    .route("/", get(handler))
    .layer(map_request(require_token))
    .layer(map_response(add_header));
  send(&app, "/", Some("x")).await;
  send(&app, "/", None).await;

  println!("[from_extractor::<Auth>, handler also takes Auth]");
  let app = Router::new()
    .route("/", get(handler_with_auth))
    .route_layer(from_extractor::<Auth>());
  for token in [Some("secret"), Some("wrong")] {
    AUTH_RUNS.store(0, Ordering::SeqCst);
    send(&app, "/", token).await;
    println!("  Auth::from_request_parts ran {} time(s)", AUTH_RUNS.load(Ordering::SeqCst));
  }
}

后面几节分别用到程序的各段。M2 Max、rustc 1.99.0,连跑 3 次输出相同,第一段:

[from_fn with Query + HeaderMap]
    mw: lang=zh ua=Some("demo")
    handler
  /?lang=zh token=None -> 200 x-mw=None body=b"ok"
  / token=None -> 400 x-mw=None body=b"Failed to deserialize query string: missing field `lang`"

第二个请求没有 lang,Query 的 rejection 直接成了 400,中间件函数没打印,handler 也没跑。中间件里的提取器失败和 handler 里的一样,状态码见 Path、Query 与请求元数据。

📝NOTE
冷知识:from_fn 的参数在 0.6 之前只能是 (req, next)

#1088(首次发布于 axum-v0.6.0-rc.1,正式版 0.6.0)让 from_fn 的函数可以在前面放提取器。PR 描述:「Lately we've had a few people ask about how run extractors from middleware. This was previously possible by building your own RequestParts and then running the extractor. But this adds support to middleware::from_fn to just support extractors directly. I think that is quite clean and doesn't require any new API.」在那之前,中间件要自己从请求造一个 RequestParts 再调提取器。合并时提取器还是基于 RequestParts 的;同在 0.6.0-rc.1 发布的 #1272 引入 FromRequestParts、删掉了 RequestParts,FromFn::call 里那段 $ty::from_request_parts(&mut parts, &state) 才成了今天的样子。

Request 必须是 Next 前面那一个。把它放到 HeaderMap 前面:

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

报错只说 FromFn<...> 没有实现 Service(rustc 1.99.0,两条相同错误中的第一条,long-type 文件路径的那行注释已删):

error[E0277]: the trait bound `FromFn<fn(Request<Body>, _, Next) -> _ {mw}, (), Route, _>: Service<_>` is not satisfied
   --> src/bin/req_not_last.rs:8:77
    |
  8 |   let _app: Router = Router::new().route("/", get(|| async { "ok" })).layer(from_fn(mw));
    |                                                                       ----- ^^^^^^^^^^^ unsatisfied trait bound
    |                                                                       |
    |                                                                       required by a bound introduced by this call
    |
    = help: the trait `tower_service::Service<axum::http::Request<Body>>` is not implemented for `FromFn<fn(Request<Body>, HeaderMap, Next) -> _ {mw}, (), Route, _>`
    = help: the following other types implement trait `tower_service::Service<Request>`:
              axum::middleware::FromFn<F, S, I, (T1, T2)>
              axum::middleware::FromFn<F, S, I, (T1, T2, T3)>
              axum::middleware::FromFn<F, S, I, (T1, T2, T3, T4)>
              axum::middleware::FromFn<F, S, I, (T1, T2, T3, T4, T5)>
              axum::middleware::FromFn<F, S, I, (T1, T2, T3, T4, T5, T6)>
              axum::middleware::FromFn<F, S, I, (T1, T2, T3, T4, T5, T6, T7)>
              axum::middleware::FromFn<F, S, I, (T1, T2, T3, T4, T5, T6, T7, T8)>
              axum::middleware::FromFn<F, S, I, (T1, T2, T3, T4, T5, T6, T7, T8, T9)>
            and 8 others
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`

和 handler 的 #[debug_handler] 对应,axum-macros 有 #[debug_middleware](要开 macros feature;文档写明它在 release 编译时不起作用,所以用 cargo build 看)。加在 mw 上,第一条错误变成:

error: `Request<_>` consumes the request body and thus must be the last argument to the handler function
 --> src/bin/req_not_last_dbg.rs:4:18
  |
4 | async fn mw(req: Request, _headers: HeaderMap, next: Next) -> Response {
  |                  ^^^^^^^

措辞沿用了 handler 的(「to the handler function」),意思一样:Next 之前的最后一个参数才能消费 body。

Next:一个装箱的服务

Next 只有一个字段:

#[derive(Debug, Clone)]
pub struct Next {
    inner: BoxCloneSyncService<Request, Response, Infallible>,
}

impl Next {
    /// Execute the remaining middleware stack.
    pub async fn run(mut self, req: Request) -> Response {
        match self.inner.call(req).await {
            Ok(res) => res,
            Err(err) => match err {},
        }
    }
}
  • 类型里没有泛型,所以中间件函数的签名能写成固定的 Next。代价是装箱:BoxCloneSyncService 是 tower 的类型擦除(类型擦除),每个请求 BoxCloneSyncService::new 一次,run 里调用时 future 再装箱一次。
  • run 拿走 self,调用一次就没了。Next 实现了 Clone 和 Service<Request>(impl Service<Request> for Next,#1712 加的),所以可以克隆后再调,或者把它当成普通 tower 服务交给别的组件。
  • run 直接 .call,没有先 poll_ready:交给 Next 的是 FromFn::call 里换出来的那个已就绪的 inner。

每层 from_fn 每个请求要多付几次堆分配?用一个计数的全局分配器,比较几种挂法。先 with_state(())(axum::serve 也会这么做,见 一个请求穿过 axum),预热一次,再发 10 000 个请求取平均。请求在计数前就造好了,不算在内:

axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

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

struct Counting;

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

unsafe impl GlobalAlloc for Counting {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    ALLOCS.fetch_add(1, Ordering::Relaxed);
    unsafe { System.alloc(layout) }
  }
  unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    unsafe { System.dealloc(ptr, layout) }
  }
}

#[global_allocator]
static GLOBAL: Counting = Counting;

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

async fn pass_map(req: Request) -> Request {
  req
}

fn ident(req: Request) -> Request {
  req
}

async fn measure(name: &str, app: Router) {
  let app = app.with_state(()); // turn handlers into Route up front
  let req = || Request::get("/").body(Body::empty()).unwrap();
  app.clone().oneshot(req()).await.unwrap(); // warm up

  const N: usize = 10_000;
  let mut reqs: Vec<_> = (0..N).map(|_| req()).collect();
  let before = ALLOCS.load(Ordering::Relaxed);
  for r in reqs.drain(..) {
    let res = app.clone().oneshot(r).await.unwrap();
    drop(res);
  }
  let allocs = (ALLOCS.load(Ordering::Relaxed) - before) as f64 / N as f64;
  println!("{name:<26} allocs/req = {allocs:.1}");
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let base = || Router::new().route("/", get(|| async { "ok" }));
  measure("no middleware", base()).await;
  measure("tower MapRequestLayer", base().layer(MapRequestLayer::new(ident))).await;
  measure("map_request", base().layer(map_request(pass_map))).await;
  measure("from_fn x1", base().layer(from_fn(pass))).await;
  measure("from_fn x2", base().layer(from_fn(pass)).layer(from_fn(pass))).await;
  measure("from_fn x3", base().layer(from_fn(pass)).layer(from_fn(pass)).layer(from_fn(pass))).await;
  measure("MapRequestLayer x2", base().layer(MapRequestLayer::new(ident)).layer(MapRequestLayer::new(ident))).await;
  measure("MapRequestLayer x3", base().layer(MapRequestLayer::new(ident)).layer(MapRequestLayer::new(ident)).layer(MapRequestLayer::new(ident))).await;
  let sb = ServiceBuilder::new()
    .layer(MapRequestLayer::new(ident))
    .layer(MapRequestLayer::new(ident))
    .layer(MapRequestLayer::new(ident));
  measure("ServiceBuilder MapReq x3", base().layer(sb)).await;
  let sb = ServiceBuilder::new().layer(from_fn(pass)).layer(from_fn(pass)).layer(from_fn(pass));
  measure("ServiceBuilder from_fn x3", base().layer(sb)).await;
}

连跑 3 次输出相同:

no middleware              allocs/req = 11.0
tower MapRequestLayer      allocs/req = 13.0
map_request                allocs/req = 14.0
from_fn x1                 allocs/req = 16.0
from_fn x2                 allocs/req = 23.0
from_fn x3                 allocs/req = 32.0
MapRequestLayer x2         allocs/req = 16.0
MapRequestLayer x3         allocs/req = 20.0
ServiceBuilder MapReq x3   allocs/req = 13.0
ServiceBuilder from_fn x3  allocs/req = 22.0

两个因素叠在一起:

  • 中间件自己的分配。 三个都放进一个 ServiceBuilder、只调用一次 Router::layer 时,Route 的层数不变,差别只来自中间件本身:tower 的 MapRequest 一次也不分配(tower 卷 service_fn 与 map 一族:「它们的 future 都不装箱」),3 层还是 13;from_fn 3 层是 22,每层多 3 次。map_request 比 MapRequestLayer 多 1 次,是它的 Box::pin。from_fn 的分配在上面那段 call 里都能看到:Box::pin 的 future、给 Next 装箱的 BoxCloneSyncService::new、Next::run 调用时装箱的 future、克隆内层服务。
  • 每次 Router::layer 多一层 Route。 Route::layer 是 Route::new(layer.layer(self)),每调一次 .layer 就多一层 BoxCloneSyncService。Route 被调用时会克隆自己的盒子(oneshot_inner 里的 self.0.clone()),克隆一个外层服务又会连带克隆它里面的 Route,层数越多,每次克隆要复制的盒子越多。MapRequestLayer 分三次 .layer,增量是 2、3、4;from_fn 分三次是 5、7、9。axum 仓库的 issue #3913(2026-10 提出,尚未关闭)报告的就是这个:「Every level then clones the rest of the stack again when it calls the next one, so the number of clones per request grows faster than the number of layers」,提议把 Route 里的盒子放进 Arc。

实用的结论和 axum 文档推荐的写法一致:一组中间件放进一个 ServiceBuilder,只调用一次 Router::layer。3 层 from_fn 从 32 次降到 22 次。

from_fn_with_state:中间件自己的状态

from_fn_with_state(state, f) 里的 state 存在 FromFnLayer 上,Layer::layer 时克隆一份给每个 FromFn,call 时再克隆一份给提取器用(上面 call 里的 let state = self.state.clone();)。它和 Router 的状态没有关系:Router::with_state 不会把状态传给中间件,要用就自己再传一份,通常是 from_fn_with_state(app_state.clone(), mw)。中间件函数里的 State<T> 和 handler 里一样经 FromRef 从这份状态里取(状态:State 与 FromRef)。

第二段,Limits 只给中间件用,路由器本身没有状态:

[from_fn_with_state]
    handler
  /short token=None -> 200 x-mw=None body=b"ok"
  /much-too-long token=None -> 414 x-mw=None body=b""

中间件函数返回 Result<Response, StatusCode>:Out 只要求 IntoResponse,Result<T, E> 在 T、E 都实现 IntoResponse 时就满足,Err 那一支就是提前结束。

map_request 与 map_response

map_request 的函数不拿 Next,只返回请求。返回值的约束是 IntoMapRequestResult,一个封闭的 trait,只有两个实现:

impl<B, E> IntoMapRequestResult<B> for Result<Request<B>, E>
where
    E: IntoResponse,
{
    fn into_map_request_result(self) -> Result<Request<B>, Response> {
        self.map_err(IntoResponse::into_response)
    }
}

impl<B> IntoMapRequestResult<B> for Request<B> {
    fn into_map_request_result(self) -> Result<Request<B>, Response> {
        Ok(self)
    }
}

call 里拿到 Ok(req) 就调用内层,Err(res) 就直接返回这个响应:

                    match f($($ty,)* $last).await.into_map_request_result() {
                        Ok(req) => {
                            ready_inner.call(req).await.into_response()
                        }
                        Err(res) => {
                            res
                        }
                    }

所以 map_request 能提前结束,写鉴权、限流这类「放行或拒绝」的逻辑比 from_fn 少一个参数。它在 Box::pin 之外不装箱 Next,上一节的分配也比 from_fn 少。

map_response 的函数最后一个参数是 Response<ResBody>,前面可以有 FromRequestParts 提取器。它的 call 先跑提取器,再调内层,最后把响应交给你:

                    match ready_inner.call(req).await {
                        Ok(res) => {
                            f($($ty,)* res).await.into_response()

函数被调用时内层已经执行完,它能改响应、换响应,但不能阻止请求进入内层。返回 Result 也可以编译(Result 实现了 IntoResponse),只是 Err 也只是换了一个响应。提取器在内层执行之前就跑完了,提取失败同样直接返回 rejection,内层不会被调用。

第三段,.layer(map_request(require_token)).layer(map_response(add_header)),map_response 后写,在外层:

[map_request + map_response]
    handler
  / token=Some("x") -> 200 x-mw=Some("map_response") body=b"ok"
  / token=None -> 401 x-mw=Some("map_response") body=b""

401 来自 map_request 的 Err(StatusCode::UNAUTHORIZED),它也带着 x-mw:外层的 map_response 照样拿到了这个响应。顺序反过来写,401 就不会有这个头(tower 卷 层的顺序反了 的复现)。

from_extractor:把提取器当中间件

from_extractor::<E>() 没有函数,只有一个类型参数。它的 Service 实现 要求 E: FromRequestParts<S>,不能是消费 body 的提取器。call 先装好一个提取的 future,响应 future 是一个两态的状态机:

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

        let extract_future = Box::pin(async move {
            let extracted = E::from_request_parts(&mut parts, &state).await;
            let req = Request::from_parts(parts, body);
            (req, extracted)
        });

        ResponseFuture {
            state: State::Extracting {
                future: extract_future,
            },
            svc: Some(self.inner.clone()),
        }
    }

ResponseFuture::poll 在 Extracting 状态里:提取成功(Ok(_))就调用内层、转到 Call 状态;失败就把 rejection into_response() 返回。注意 Ok(_):提取出来的值被丢掉了,handler 拿不到。另外两点:

  • 它的 Error 是内层的 T::Error,不像其他三个固定为 Infallible,也不要求内层的错误是 Infallible。
  • 调用的是 self.inner.clone() 出来的副本,没有用 mem::replace 换出就绪的那个。内层是 Route 时无所谓(Route::poll_ready 永远就绪),内层是需要就绪的 tower 服务时,就会碰到 poll_ready must be called first 那个问题。

axum 文档对它的定位是:「You have a type that you sometimes want to use as an extractor and sometimes as a middleware. If you only need your type as a middleware prefer middleware::from_fn.」

❗IMPORTANT
动脑筋:程序最后一段,/ 用 route_layer(from_extractor::<Auth>()) 保护,handler 自己也要一个 Auth 参数。带正确 token 时 Auth::from_request_parts 跑几次?带错误 token 呢?答案在本页最后。

什么时候写 from_fn

axum 文档 middleware.md 的「Writing middleware」 列了四档写法。按本页看到的代价整理:

写法能用提取器中间件自身每个请求的分配能给别的 tower 栈用
from_fn / from_fn_with_state能Box::pin 的 future、Next 的盒子和它的 future不能:要求内层 Error = Infallible、请求是 axum 的 Request
map_request / map_response能Box::pin 的 future同上
from_extractor只有那一个提取器提取 future 的 Box::pin内层错误类型不限,但提取器是 axum 的 trait;提取结果丢掉
tower ServiceBuilder::map_request 等不能无能
手写 Service + Layer不能(自己从 Parts 取)由你决定能,可以发布成 crate

文档对 from_fn 的建议是:不想自己写 future、并且「You don't intend to publish your middleware as a crate for others to use. Middleware written like this are only compatible with axum」。在乎分配次数时,先看上一节的数字:把多次 .layer 合并进一个 ServiceBuilder,3 层 from_fn 省下的 10 次,比 from_fn 和 MapRequestLayer 之间每层 3 次的差距大。

动脑筋答案

程序最后一段的输出(连跑 3 次相同):

[from_extractor::<Auth>, handler also takes Auth]
    handler
  / token=Some("secret") -> 200 x-mw=None body=b"ok"
  Auth::from_request_parts ran 2 time(s)
  / token=Some("wrong") -> 401 x-mw=None body=b"bad token"
  Auth::from_request_parts ran 1 time(s)
  • 带正确 token:2 次。 from_extractor 跑一次,成功后把结果丢掉(Ok(_));请求进入 handler,Auth 作为参数又提取一次。中间件和 handler 之间没有共享这个值。
  • 带错误 token:1 次。 中间件里的那次就失败了,rejection (401, "bad token") 直接返回,handler 没被调用。

所以文档说「sometimes as an extractor and sometimes as a middleware」:同一个类型在一些路由上当中间件挡住请求,在另一些路由上当参数取值。两处都用就要付两次的代价;提取器里如果有查数据库这样的操作,把结果放进 extensions、让 handler 用 Extension<T> 取,或者改用 from_fn 自己插。

小结

  • from_fn(f) 是状态为 () 的 from_fn_with_state;FromFn::call 用 mem::replace 换出就绪的内层,跑提取器,把内层装箱成 Next,再调用 f。
  • 中间件函数的参数规则和 handler 一样:前面 FromRequestParts,Next 前一个 FromRequest(通常是 Request),提取失败直接返回 rejection。参数写错时 #[debug_middleware](debug 编译)给出可读的报错。
  • Next 是 BoxCloneSyncService<Request, Response, Infallible>,每个请求新装一次;实测一层 from_fn 比 tower MapRequestLayer 每个请求多 3 次分配,分多次 .layer 还会因为 Route 层层嵌套而越来越多(issue #3913)。把中间件合进一个 ServiceBuilder 再 .layer 一次。
  • from_fn_with_state 的状态与 Router 的状态无关,要自己传。
  • map_request 返回 Result<Request, E> 可以提前结束;map_response 在内层之后执行,不能挡请求。
  • from_extractor 只接受 FromRequestParts 提取器,提取结果被丢弃,handler 再用同一个提取器会再跑一次。

💬 关联讨论

发起讨论
加载中...