Axum 设计与实现

第13章 from_fn 中间件:提取器 + Next 的函数式模型

作者 杨艺韬 · 12,782 字 · 发布于 · 更新于

前面几章讲完请求到响应的完整链路——提取器、handler、响应、错误处理。把一个请求从 hyper 送到 handler 再把响应送回 hyper 的路径上还有一个重要环节:中间件。HTTP 日志、认证、限流、压缩、CORS——都是中间件的工作。

Axum 的中间件有两种风格:

  1. 原生 tower::Layer + tower::Service:能力最全、也最繁琐。需要实现 Service trait、处理 poll_ready / call、写 Future 类型
  2. axum::middleware::from_fn:把中间件写成一个 async fn(request, next) -> Response 就完事。适合 80% 的场景

第 14 章会讲 map_request / map_response / from_extractor 等介于两者之间的变体。本章专注 from_fn——它是 axum 最常用的中间件编写方式。

为什么需要"函数式中间件"

直接写 Layer + Service 实现一个"给请求加 header"的中间件大概这样:

// Tower 原生写法(简化)
struct AddHeaderLayer { name: HeaderName, value: HeaderValue }

impl<S> Layer<S> for AddHeaderLayer {
    type Service = AddHeader<S>;
    fn layer(&self, inner: S) -> Self::Service { /* ... */ }
}

struct AddHeader<S> { inner: S, name: HeaderName, value: HeaderValue }

impl<S> Service<Request<Body>> for AddHeader<S>
where S: Service<Request<Body>, Response = Response<Body>>,
{
    type Response = Response<Body>;
    type Error = S::Error;
    type Future = /* 复杂的 pin_project future 类型 */;

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

    fn call(&mut self, mut req: Request<Body>) -> Self::Future {
        req.headers_mut().insert(self.name.clone(), self.value.clone());
        self.inner.call(req)
    }
}

几十行代码——而且还是简单场景。如果要跨 await 访问请求、修改响应,Future 类型会进一步复杂(pin_project、State enum for pending / ready 状态)。

from_fn 让同样的工作变成:

use axum::{middleware, extract::Request, middleware::Next, response::Response};

async fn add_header(mut req: Request, next: Next) -> Response {
    req.headers_mut().insert("x-axum-test", "ok".parse().unwrap());
    next.run(req).await
}

// 用法
let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(add_header));

三行函数体——写起来像写普通 async 函数。内部细节(Layer/Service 包装、Future 类型构造、poll_ready 处理)全被 from_fn 吞掉。

from_fn 的目标不是替代 Tower——生态里大量中间件(TraceLayer、CorsLayer、TimeoutLayer)仍然用原生 Layer + Service——而是给"简单的自定义业务中间件"一个低门槛入口。

middleware 函数的 FnMut 约束细节

from_fn 生成的 Service 要求 F 是 FnMut + Clone + Send + 'static——几乎所有 async fn 都自动满足。但如果写成闭包,捕获了某些类型就可能不满足:

  • 捕获 !Send 类型(如 Rc<T>)→ 编译失败。解决:用 Arc<T> 替换
  • 捕获 !Clone 类型(如 TcpStream)→ 闭包不 Clone,挂到 Router 上时编译失败
  • 捕获非 'static 引用(如本地变量的 &str)→ 'static bound 失败。解决:move 一份 own 的 String / Arc<str>

大多数情况你写 async fn + 用 State<T> 取依赖,这些问题不会遇到。只有写成闭包 + 手动 move 捕获时会碰到。

设计决策:为什么单独搞一套 from_fn 而不是扩展 tower::Layer

一个合理的反问:Tower 能不能让 Layer 本身就接受 async fn?Tower 只提供了 service_fn(把 async 闭包变成 Service)和 layer_fn(用闭包造 Layer),没有"拿到剩余的栈、前后插逻辑"这种 async fn 中间件——Tower 对请求、响应类型完全泛型,没法把"剩余的栈"擦成一个固定类型交给用户函数(见下面第一点)。

axum 选择自己做 from_fn,做了几个针对 HTTP 场景的专门优化:

一、固定 Request/Response 类型:from_fn 写死了 Request 和 Response(axum 的具体类型)。这让 Next 的类型擦除(BoxCloneSyncService)成为可能——泛型擦除依赖固定的输入输出类型。Tower 的通用 Layer 不能这样——它要处理 Service<Req, Response = Res> 的任意 Req/Res。

二、提取器支持:axum 知道自己的 FromRequestParts / FromRequest trait,所以 from_fn 可以自动注入提取器。Tower 不知道这些 trait,不能提供类似支持。

三、Error = Infallible 的统一契约:axum 要求所有 Service Error 是 Infallible(第 12 章)。from_fn 的签名 -> Response 或 -> Result<Response, E: IntoResponse> 自然适配这个契约——E 转成 Response、最终 Service Error 是 Infallible。Tower Layer 不能假设这个契约。

这三点让 from_fn 成为"axum specific"的封装——既有语法便利、又和 axum 的其他机制(提取器、错误处理)无缝配合。牺牲了跨 Tower 项目复用性——但 axum 内部用着舒服。

Next:剩余中间件栈的抽象

from_fn 的中间件函数签名必须有一个 next: Next 参数——Next 是 axum 对"调用中间件栈剩余部分"的抽象。源码在 axum/src/middleware/from_fn.rs:336-350:

// axum/src/middleware/from_fn.rs:336-350(省略文档注释)
#[derive(Debug, Clone)]
pub struct Next {
    inner: BoxCloneSyncService<Request, Response, Infallible>,
}

impl Next {
    pub async fn run(mut self, req: Request) -> Response {
        match self.inner.call(req).await {
            Ok(res) => res,
            Err(err) => match err {},
        }
    }
}

Next 内部持有 BoxCloneSyncService<Request, Response, Infallible>——一个类型擦除的 Service 对象,入参是 Request、响应是 Response、Error 是 Infallible。Error = Infallible 是关键——第 12 章讨论过的"axum Service 永远 Infallible"保证让 Next::run 的签名是 async fn run(self, req) -> Response(不是 -> Result<Response, E>)——调用者用起来像普通 async 函数。

match err {} 是 Infallible 的典型用法——空 match 穷尽所有(零个)变体,编译器优化掉这行机器码。

中间件函数拿到 Next 后可以做三件事:

  1. 不调 next.run(req):短路中间件栈——比如认证失败直接返 401,handler 不执行
  2. 调 next.run(req).await 一次:正常透传——可以在调用前改 req、调用后改 response
  3. 调用多次(罕见):重试逻辑?Next::run 按值消费 self,同一个 Next 只能 run 一次;但 Next 实现了 Clone,可以先 next.clone() 再各 run 一次——真正的障碍是 request 的 body 只能消费一次,每次都得重建 request
flowchart LR
    REQ[Request] --> MW["中间件函数\nasync fn(req, next)"]
    MW -->|"修改 req"| REQ2[修改后的 Request]
    REQ2 -->|"next.run(req).await"| INNER["剩余栈<br/>(Next)"]
    INNER -->|"Response"| MW
    MW -->|"修改 res"| RES[最终 Response]
    MW -.->|"短路: 直接返响应"| SHORT[不进入 INNER]

BoxCloneSyncService:为什么需要类型擦除

Next 的 inner 是 BoxCloneSyncService<Request, Response, Infallible>——一个 tower util 提供的"Service trait object"。为什么不用具体类型?

因为 Next 要能被 async 函数按值消费。async fn middleware(req, next: Next) 里 next 的类型必须是具体的——不能是 <S: Service<...>> 这样的泛型(中间件函数由用户写好后传给 from_fn,内层 Service 的具体类型是层层 Layer 嵌套出来的,用户既写不出、也不该关心)。

类型擦除让 Next 有一个确定的具体类型——代价是虚函数调用加堆分配:BoxCloneSyncService::new 把 inner 装箱一次,next.run 时 inner 返回的 future 再被 Box::pin 一次(tower/src/util/boxed_clone_sync.rs:32-33)。在中间件场景下这层开销可以忽略——中间件本身不在纳秒级热路径上。

BoxCloneSyncService 比 BoxService 多要求 Clone + Sync——axum 需要 Service 能被多个请求共享(因为一个 Router 被多个连接并发使用)。这个变体是 tower 0.5.2 才加入的(tower #777);在此之前 axum 自己维护一个 Sync 版的 BoxCloneService,0.8.0 起换成 tower 的(#3109)。

from_fn 对中间件函数的签名要求

from_fn 文档(axum/src/middleware/from_fn.rs:25-29)明确列出签名约束:

  1. Be an async fn.
  2. Take zero or more FromRequestParts extractors.
  3. Take exactly one FromRequest extractor as the second to last argument.
  4. Take Next as the last argument.
  5. Return something that implements IntoResponse.

和第 5 章讲的 handler 签名几乎一样——除了最后多一个 Next 参数。这不是巧合:middleware 和 handler 在 axum 眼里是同一种东西——都是"处理 Request 产出 Response 的 async fn"——差别只在 middleware 能透传给下一层。

合法签名举例:

// 最简:只有 req + next
async fn m1(req: Request, next: Next) -> Response;

// 带前置提取器:可以提取 headers / method / uri 等
async fn m2(method: Method, headers: HeaderMap, req: Request, next: Next) -> Response;

// 返回 Result:Err 也要 IntoResponse
async fn m3(headers: HeaderMap, req: Request, next: Next) -> Result<Response, StatusCode>;

// 最后一个提取器可以消费 body(任何 FromRequest<S>,如 String / Bytes)
async fn m4(method: Method, body: String, next: Next) -> Response;  // 拿走 body,但此后没有 Request 可交给 next.run,只能自己重建或短路

注意规则 3:"second to last 是 FromRequest"——因为 body 只能消费一次(第 6 章讨论过)。如果 middleware 不想消费 body,倒数第二个参数通常用 Request(自身实现 FromRequest 的恒等 impl)——这等于"拿到完整 request 但不动 body"。

FromFn 的 Service 实现

中间件函数经过 from_fn(f) 变成 FromFnLayer<F, S, T>,layer(inner) 生成 FromFn<F, S, I, T>——最终的 Service。核心 impl 在 axum/src/middleware/from_fn.rs:254-318 的 impl_service! 宏里,由 all_the_tuples! 展开 16 份(axum-core/src/macros.rs 的 all_the_tuples! 从 [], T1 一路排到 [T1..T15], T16——也就是 0-15 个前置 FromRequestParts 参数 + 最后一个 FromRequest 参数):

// axum/src/middleware/from_fn.rs:258-316 (简化, 三参数版本)
impl<F, Fut, Out, S, I, T1, T2, T3> Service<Request>
    for FromFn<F, S, I, (T1, T2, T3)>
where
    F: FnMut(T1, T2, T3, Next) -> Fut + Clone + Send + 'static,
    T1: FromRequestParts<S> + Send,
    T2: FromRequestParts<S> + Send,
    T3: FromRequest<S> + Send,
    Fut: Future<Output = Out> + Send + 'static,
    Out: IntoResponse + 'static,
    I: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
    I::Response: IntoResponse,
    I::Future: Send + 'static,
    S: Clone + Send + Sync + 'static,
{
    type Response = Response;
    type Error = Infallible;
    type Future = ResponseFuture;

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

    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 {
            // 依次调用 FromRequestParts 提取器
            let T1 = match T1::from_request_parts(&mut parts, &state).await { ... };
            let T2 = match T2::from_request_parts(&mut parts, &state).await { ... };

            // 重建 Request, 调用最后一个 FromRequest 提取器
            let req = Request::from_parts(parts, body);
            let T3 = match T3::from_request(req, &state).await { ... };

            // 包装 inner 成 Next
            let inner = BoxCloneSyncService::new(MapIntoResponse::new(ready_inner));
            let next = Next { inner };

            // 调用用户 middleware 函数
            f(T1, T2, T3, next).await.into_response()
        });

        ResponseFuture { inner: future }
    }
}

逐段拆解。

poll_ready 转发

self.inner.poll_ready(cx)——简单转发给 inner。from_fn 本身不引入 back-pressure(中间件函数是 async fn,没有 ready 概念),内层 Service 的 ready 状态直接向上传。

clone-and-replace 模式

第 12 章讨论过的 Tower 惯用模式——let ready_inner = std::mem::replace(&mut self.inner, clone)。ready_inner 进入 async block 被用(处于 ready 状态,call 合法),self.inner 留的是未 ready 的 clone 副本。

提取器顺序

前 N-1 个参数(FromRequestParts)通过 from_request_parts(&mut parts, &state) 依次提取;最后一个(FromRequest)通过 from_request(req, &state) 消费整个请求——和第 5 章 handler 完全一样。state 从 self.state.clone() 取(from_fn_with_state 提供的 state),没有 state 时是 ()。

MapIntoResponse:让 inner 的 Response 类型归一

MapIntoResponse::new(ready_inner) 是 axum 内部的 adapter——把一个 Service<Response = T: IntoResponse> 包装成 Service<Response = Response>。为什么需要?

因为 Next 的 inner 类型是 BoxCloneSyncService<Request, Response, Infallible>——写死了 Response 类型是 Response。但 Layer 包装的 inner Service 的 Response 可能是其他 IntoResponse 类型(比如 Service<Response = String>)。MapIntoResponse 在 call 时做 response.into_response() 转换,让 inner 能塞进 BoxCloneSyncService。

这是"类型擦除 + 自动适配"的典型手法——不让用户感知内部的类型归一。

ResponseFuture 的结构

axum/src/middleware/from_fn.rs:367-377:

pub struct ResponseFuture {
    inner: BoxFuture<'static, Response>,
}

impl Future for ResponseFuture {
    type Output = Result<Response, Infallible>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.inner.as_mut().poll(cx).map(Ok)
    }
}

inner 是 BoxFuture<'static, Response>——直接返回 Response 的 future。poll 里 .map(Ok) 把 Response 包装成 Result<Response, Infallible> 适配 Service trait——和第 5 章 HandlerService 用同样手法。

整条 from_fn 的内部数据流:

sequenceDiagram
    participant Client
    participant Outer as Outer Layer
    participant FromFn as FromFn Service
    participant Ext as 前置提取器
    participant F as middleware fn
    participant Next as Next
    participant Inner as Inner Service
    Client->>Outer: Request
    Outer->>FromFn: call(req)
    FromFn->>FromFn: clone state, replace inner
    FromFn->>Ext: T1::from_request_parts(parts, state)
    Ext-->>FromFn: Ok(T1)
    FromFn->>Ext: T2::from_request_parts(parts, state)
    Ext-->>FromFn: Ok(T2)
    FromFn->>FromFn: 重建 Request
    FromFn->>Ext: T_last::from_request(req, state)
    Ext-->>FromFn: Ok(T_last)
    FromFn->>FromFn: 包 inner 成 Next
    FromFn->>F: f(T1, T2, T_last, next)
    F->>Next: next.run(req)
    Next->>Inner: inner.call(req)
    Inner-->>Next: Response
    Next-->>F: Response
    F-->>FromFn: 返回值 into_response()
    FromFn-->>Client: Response

from_fn_with_state:注入共享 state

from_fn 不接受 state——from_fn_with_state(state, f) 才行。区别是后者让 middleware 函数能用 State<T> 提取器:

async fn auth_middleware(
    State(db): State<PgPool>,  // 从 state 提取
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    // 用 db 做认证
    let user = db.check_token(/* ... */).await.map_err(|_| StatusCode::UNAUTHORIZED)?;
    Ok(next.run(request).await)
}

let app = Router::new()
    .route("/protected", get(handler))
    .route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware))
    .with_state(state);

state.clone() 传给 from_fn_with_state——middleware 的 state;另一个 state 传给 .with_state——handler 的 state。两份必须一致。

from_fn 是 from_fn_with_state((), f) 的快捷方式(axum/src/middleware/from_fn.rs:114-116)——内部 state 是 ()、提取器 bound 是 FromRequestParts<()>。所以 from_fn 能用 Method / HeaderMap 这种不依赖 state 的提取器,不能用 State<T>。

何时用 from_fn、何时用 tower::Layer

两种风格的适用场景:

场景 from_fn tower::Layer
写一个简单的业务中间件 ✓ 过度工程
需要 poll_ready 的真正背压逻辑 ✗ ✓
需要精细控制 Service 内部状态 ✗ ✓
想给 axum 之外的项目用(其他 tower 用户) ✗ ✓
多个 Router 反复用同一个中间件 都行 更合适
需要从中间件函数内访问类似 async fn 的控制流 ✓ 麻烦
性能极度敏感 ✗(每请求多三次 box 分配) ✓
需要 FromRequestParts 提取器 ✓ 自己写要多写很多代码

简单判断:如果你只是想"在 handler 前后插点逻辑",用 from_fn。如果要做"真正的 Service 改造"(背压、流量控制、连接级状态),用 Layer。

from_fn 的开销是确定的:每次请求一次 Box::pin 包住整个中间件 future(一次堆分配),一次 BoxCloneSyncService::new(一次堆分配 + 之后每次调用一次虚函数派发),调用 next.run 时内层 future 再被 Box::pin 一次(第三次堆分配)。对毫秒级的 handler 业务可以忽略;只有在亚微秒级的热路径上才需要在意。

中间件的叠加与执行顺序

多个 from_fn 叠加时的执行顺序是下一个必须理解的概念。看这段代码:

let app = Router::new()
    .route("/", get(handler))
    .layer(from_fn(inner_mw))     // 最先加 → 最靠内
    .layer(from_fn(middle_mw))
    .layer(from_fn(outer_mw));    // 最后加 → 最靠外

Tower 的 .layer(X) 是"把 X 包在当前 Service 外面"——即后添加的 Layer 在请求流向上更靠外。所以上面的执行顺序是:

请求 → outer_mw → middle_mw → inner_mw → handler
响应 ← outer_mw ← middle_mw ← inner_mw ← handler

每个中间件的代码结构都是"next.run 前做点事、next.run 后做点事"——这形成了洋葱模型:

flowchart TB
    subgraph "洋葱模型"
        direction TB
        O1["outer_mw::Before"]
        M1["middle_mw::Before"]
        I1["inner_mw::Before"]
        H["handler"]
        I2["inner_mw::After"]
        M2["middle_mw::After"]
        O2["outer_mw::After"]
        O1 --> M1 --> I1 --> H --> I2 --> M2 --> O2
    end
    REQ[请求] --> O1
    O2 --> RES[响应]

每个中间件的"before"代码在 next.run 之前运行、"after"代码在 next.run 之后——Before 按最外到最里顺序、After 按最里到最外逆序。这和其他框架(Express、Koa、Django middleware)的经典洋葱模型一致——但在 axum 里它是类型系统自然派生的结果,不是额外约定。

顺序在生产里的影响:

  • 日志在最外:记录完整生命周期的时间,被认证拒掉的请求也留得下记录
  • 认证紧随其后:请求没认证通过就别让内层中间件浪费 CPU
  • rate limit 在认证之后:rate limit 通常按用户/API key 计数,需要先认证拿到身份
  • 压缩(compression)放在要读写响应 body 的中间件之外:否则它们拿到的是已经压缩过的字节

这些是经验原则——没有框架强制,用户自己决定 Layer 顺序。错误的顺序不会编译失败但会行为不对。

实战一:认证中间件

from_fn 的规范场景就是认证——检查请求 header、拿到用户、决定放行或拒绝:

use axum::{
    extract::{Request, State},
    http::{HeaderMap, StatusCode},
    middleware::{self, Next},
    response::Response,
};

async fn auth(
    State(state): State<AppState>,
    headers: HeaderMap,
    mut request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let token = headers
        .get("authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let user = state.auth_service.verify_token(token)
        .await
        .map_err(|_| StatusCode::UNAUTHORIZED)?;

    // 把 user 塞进 request extensions, 后续 handler 可以提取
    request.extensions_mut().insert(user);

    Ok(next.run(request).await)
}

let app = Router::new()
    .route("/protected", get(protected_handler))
    .route_layer(middleware::from_fn_with_state(state.clone(), auth))
    .with_state(state);

// handler 里用 Extension<User> 拿到认证结果
async fn protected_handler(Extension(user): Extension<User>) -> impl IntoResponse {
    format!("hello {}", user.name)
}

几个要点:

一、短路即 Err:返回 Err(StatusCode::UNAUTHORIZED) 直接变 401 响应——因为 StatusCode impl IntoResponse。Result<Response, StatusCode> 的两边都能变 Response,这让 ? 无缝工作。

二、往 extensions 塞 user:handler 通过 Extension<User> 拿到——中间件和 handler 之间通过 extensions 传递类型化数据。第 7 章对比 Extension<T> 与 State<T> 时提过这条路径。

三、route_layer vs layer:route_layer 只包住调用它之前已经注册的路由(之后再加的路由不受影响);layer 同样只包调用时已注册的路由,但连 fallback 一起包。认证通常用 route_layer——请求不存在的路径时返回 404 而不是 401。fallback 是否被 route_layer 包住,0.8.9 与主线不同(实测,中间件一律返回 401):

  • 0.8.9:fallback 存在单独的 fallback_router 里,route_layer 只改 path_router,所以无论 .fallback() 写在前还是后、嵌套子 Router 带不带 fallback,未匹配请求都拿到 404
  • 主线(#3158 把 fallback 并进路由表,axum/src/routing/mod.rs:391-440 的 fallback_endpoint):没设 fallback 时默认 404 仍不受影响;但先 .fallback(h) 再 route_layer、或 nest 进一个自带 fallback 的子 Router,fallback 就会被包住,未匹配请求得到 401。想让 fallback 不过认证,就在 route_layer 之后再调 .fallback()

实战二:请求日志中间件

结构化日志通常用 tower_http::trace::TraceLayer——但有时想要更定制化的 log,用 from_fn 写更灵活:

use axum::{
    extract::Request,
    middleware::Next,
    response::Response,
};
use std::time::Instant;
use tracing::{info, info_span, Instrument};

async fn log_request(request: Request, next: Next) -> Response {
    let method = request.method().clone();
    let uri = request.uri().clone();
    let span = info_span!("request", %method, %uri);

    async move {
        let start = Instant::now();
        info!("started");
        let response = next.run(request).await;
        let elapsed = start.elapsed();
        info!(status = response.status().as_u16(), elapsed_ms = elapsed.as_millis(), "done");
        response
    }
    .instrument(span)
    .await
}

亮点:

  • tracing::info_span! + .instrument:span 覆盖 next.run 的整个执行——handler 里的 tracing 事件自动带上 request 的 span context(method、uri 作为 tag)
  • 时间测量:Instant::now + elapsed,记录处理时间
  • 不塞不必要的字段:只 log 开始/结束和状态码。如果每个字段都塞会让日志容量暴涨——按监控 / 排查需要精选

这比用 TraceLayer 定制化更灵活——但 TraceLayer 自带按请求建 span、按响应和 body 分类失败、各阶段回调(on_request / on_response / on_failure 等)这些现成功能,接 OpenTelemetry 则靠 tracing 生态(如 tracing-opentelemetry)。生产里建议:轻量自定义用 from_fn,完整可观测性用 TraceLayer + 自定义 span 配置。

实战三:基于 state 的 rate limit

rate limit 通常用 tower::limit——但某些精细策略(按 API key、按 endpoint)需要自定义:

use std::sync::Arc;
use dashmap::DashMap;
use std::time::{Duration, Instant};

#[derive(Clone)]
struct RateLimitState {
    windows: Arc<DashMap<String, (Instant, u32)>>,
    max_per_minute: u32,
}

async fn rate_limit(
    State(limit): State<RateLimitState>,
    headers: HeaderMap,
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let key = headers.get("x-api-key")
        .and_then(|v| v.to_str().ok())
        .map(String::from)
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let now = Instant::now();
    let mut entry = limit.windows.entry(key.clone()).or_insert((now, 0));

    // 一分钟重置窗口
    if now.duration_since(entry.0) >= Duration::from_secs(60) {
        *entry = (now, 0);
    }

    if entry.1 >= limit.max_per_minute {
        return Err(StatusCode::TOO_MANY_REQUESTS);
    }

    entry.1 += 1;
    drop(entry);  // 显式释放 DashMap lock, 避免跨 await 持有

    Ok(next.run(request).await)
}

关键细节:

一、DashMap 而非 std::sync::Mutex<HashMap>:DashMap 是分片锁 HashMap,并发读写性能好。Mutex<HashMap> 的锁会串行化所有访问——rate limit 中间件每个请求都会用,全局锁是瓶颈

二、drop(entry) 显式释放:entry 是 DashMap 的 guard(持有分片的写锁)。它是 Send 的,跨 await 持有照样能编译(dashmap 6.2.1 实测),但锁会一直占到 handler 返回——同一分片上的其他请求会同步阻塞 tokio 工作线程,严重时死锁。显式 drop 在 await 前让锁释放

三、内存可能无限增长:entry.or_insert(...) 对新 key 创建条目但从不清理。生产里需要定期扫描旧条目删掉(windows 上次访问时间 > 某阈值)——tokio::spawn 一个后台任务做 GC

这种固定窗口(fixed window)rate limit 仅是示例,真实 production 推荐用 tower_governor 等成熟库,或配合 Redis 做分布式 rate limit。

分布式场景的 rate limit

单机 DashMap 只能限单个进程的流量——生产里多实例部署需要共享状态。典型方案是 Redis:

async fn rate_limit_redis(
    State(redis): State<redis::aio::MultiplexedConnection>,
    headers: HeaderMap,
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let key = extract_key(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
    let redis_key = format!("ratelimit:{key}");

    let mut conn = redis.clone();

    // INCR + EXPIRE 两条命令,非原子(真正原子要用 Lua script)
    let count: u32 = redis::cmd("INCR").arg(&redis_key).query_async(&mut conn).await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if count == 1 {
        let _: () = redis::cmd("EXPIRE").arg(&redis_key).arg(60).query_async(&mut conn).await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    }

    if count > 100 {
        return Err(StatusCode::TOO_MANY_REQUESTS);
    }

    Ok(next.run(request).await)
}

几个生产要点:

  1. 用 Lua script 做原子:上面 INCR + EXPIRE 非原子,INCR 成功而 EXPIRE 失败(或进程恰好在两者之间退出)时 TTL 永远设不上、计数永不过期——推荐写一个 Lua script 原子处理
  2. 失败路径行为:Redis 宕掉时 middleware 是拒绝(安全优先)还是放行(可用性优先)?生产需要明确策略。默认建议放行——否则 Redis 单点故障让整个服务不可用(上面的写法走的是拒绝,一律 500)
  3. 连接复用:示例里的 MultiplexedConnection 本身可以 clone 共享、在一条连接上多路复用;换成非多路复用的连接时,才需要 bb8-redis / deadpool-redis 这类连接池并注意池大小
  4. 本地 + 分布式混合:结合本地(DashMap)做粗限(防突发流量直接压到 Redis)+ 分布式(Redis)做精限——混合方案更稳

这些复杂性说明:rate limit 是用 tower_governor 等成熟库的典型场景——不必自己写一套。from_fn 快速原型时手写、生产里换成库。

from_fn 的历史演进

from_fn 在 axum 不同版本的演化:

  • axum 0.4.6(2022-02-22)之前:axum 本体里没有 from_fn(它先放在 axum-extra 里)。只用 axum 本体的话,中间件都要手写 Layer/Service——门槛高
  • axum 0.4.6:CHANGELOG 里一行 "middleware::from_fn for creating middleware from async functions. This previously lived in axum-extra but has been moved to axum"(#719)是它进入 axum 本体的起点——最早版本只支持单一 (req, next) 签名,不支持提取器
  • axum 0.5.4(2022-04-26):加入 middleware::from_extractor,同时弃用旧的 extract::extractor_middleware(#957)——这是第 14 章的主角之一
  • axum 0.6.0(2022-11-25):一次性补齐三件事——from_fn 支持运行提取器(#1088)、加入 from_fn_with_state 让 middleware 能访问 state、加入 map_request / map_response 及其 _with_state 变体
  • axum 0.6.3:Next 实现 Clone 与 Service(#1712)
  • axum 0.7.0:Next 去掉请求 body 的泛型参数(Next<B> → Next,#1751);0.7.5 加入 #[debug_middleware]
  • axum 0.8.0:Next 内部从 axum 自己的 Sync 版 BoxCloneService 换成 tower 的 BoxCloneSyncService(#3109),对用户透明

每一版 from_fn 都在让中间件编写门槛更低、能力更接近 handler。到 0.8 时 from_fn 的心智模型和 handler 完全一致——signature 像 handler、能用提取器、? 错误处理一样——唯一差别是多了个 Next。

对比其他框架,axum 的 from_fn 接近 Koa / Express 的 middleware 心智模型:

// Koa
app.use(async (ctx, next) => {
    // before
    await next();
    // after
});
// axum from_fn
async fn mw(req: Request, next: Next) -> Response {
    // before
    let response = next.run(req).await;
    // after
    response
}

两者神似。差异在类型系统——axum 有提取器和 state、Koa 用 ctx 对象聚合一切。axum 用类型表达的东西 Koa 用命名对象聚合——各有优劣。

from_fn 的几种高级用法

一、按条件不调 next:authorization、feature flag、maintenance mode 等场景:

async fn maintenance(request: Request, next: Next) -> Response {
    if is_maintenance_mode() {
        (StatusCode::SERVICE_UNAVAILABLE, "under maintenance").into_response()
    } else {
        next.run(request).await
    }
}

二、给响应强制加 header(覆盖 handler 自己设的):

async fn force_security_headers(request: Request, next: Next) -> Response {
    let mut response = next.run(request).await;
    let h = response.headers_mut();
    h.insert("x-frame-options", HeaderValue::from_static("DENY"));
    h.insert("x-content-type-options", HeaderValue::from_static("nosniff"));
    response
}

三、按响应状态码做不同后处理:

async fn error_reporter(request: Request, next: Next) -> Response {
    let method = request.method().clone();
    let uri = request.uri().clone();
    let response = next.run(request).await;
    if response.status().is_server_error() {
        tracing::error!(%method, %uri, status = %response.status(), "5xx response");
    }
    response
}

四、读取并重建 request body(比如校验 header 里对 body 的 HMAC 签名):

async fn verify_hmac(request: Request, next: Next) -> Result<Response, StatusCode> {
    let (parts, body) = request.into_parts();
    let bytes = axum::body::to_bytes(body, 10 * 1024 * 1024).await
        .map_err(|_| StatusCode::PAYLOAD_TOO_LARGE)?;

    let expected_hmac = parts.headers.get("x-signature")
        .and_then(|v| v.to_str().ok())
        .ok_or(StatusCode::UNAUTHORIZED)?;

    if !verify_hmac_sig(&bytes, expected_hmac) {
        return Err(StatusCode::UNAUTHORIZED);
    }

    // body 验证通过, 重构 request 给 handler
    let request = Request::from_parts(parts, Body::from(bytes));
    Ok(next.run(request).await)
}

五、条件性应用中间件(运行时决定):这用 from_fn 做不优雅——应该在构建 Router 时决定挂不挂(cfg、if 分支,或 tower::util::option_layer 把 Option<Layer> 变成可挂的 Layer)。from_fn 内部条件更适合"每个请求的快速判断",不适合"全局 on/off"。

六、并行 middleware 分支:某些场景想"并行做多件事、都完成后才继续"(比如同时检查 authz 和 quota):

async fn parallel_checks(
    State(services): State<Services>,
    headers: HeaderMap,
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let auth_check = services.auth.verify(&headers);
    let quota_check = services.quota.check(&headers);

    // join! 并行两个 future
    let (auth_res, quota_res) = tokio::join!(auth_check, quota_check);

    auth_res.map_err(|_| StatusCode::UNAUTHORIZED)?;
    quota_res.map_err(|_| StatusCode::TOO_MANY_REQUESTS)?;

    Ok(next.run(request).await)
}

tokio::join! 在同一个任务里交替驱动两个 future——两次 I/O 等待重叠,总耗时接近两者的最大值而不是两者之和。做多个独立后端调用时(比如同时查用户表和权限表)值得用。但要注意 join 不是无限并发——future 是在同一个任务里,如果都是 CPU 密集的阻塞操作还是串行的。I/O 密集的调用(网络 RPC、数据库查询)才有加速效果。

常见 pattern 小结

几个 axum 生态常见的 from_fn middleware:

middleware 典型签名 常用场景
auth (State<S>, HeaderMap, Request, Next) -> Result<Response, StatusCode> 登录验证
log_request (Request, Next) -> Response 请求日志
rate_limit (State<S>, HeaderMap, Request, Next) -> Result<Response, StatusCode> 限流
cors_preflight (Method, Request, Next) -> Response OPTIONS 请求短路处理
request_id (Request, Next) -> Response 生成 / 传播 X-Request-Id
feature_flag_gate (State<S>, HeaderMap, Request, Next) -> Response 按用户开关 feature
request_body_size_limit (Request, Next) -> Result<Response, StatusCode> body 大小预检

这些 middleware 中大部分都能用 tower-http 或其他成熟库——但手写了解原理对调试很重要。生产项目的中间件栈通常是"2-3 个库中间件 + 3-5 个自己的 from_fn 中间件"混用。

中间件和错误处理的协作

from_fn 中间件返回 Result<Response, E> 时,E 必须 IntoResponse——短路走 E 的 into_response 产出响应。这和 handler 的错误模型一致,但有几个细节:

一、Err 只截断更内层:inner_mw 返回 Err 时,handler 和比它更靠内的中间件都不执行;但这个 Err 已经在 FromFn 里被 into_response() 成普通 Response,照常回到 middle_mw 的 next.run(...).await 处——middle_mw、outer_mw 的"after"代码照样执行,看到的就是这个错误响应(实测)。

二、HandleErrorLayer 捕获不到中间件的 Err:它只能处理 Service::Error,而 from_fn 的 Error 类型是 Infallible(短路时 Err 已经变成 Response)。所以 HandleErrorLayer 不会捕获 from_fn 中间件的短路——那已经是 Response 了。

三、panic 跟普通 async fn 一样:需要外层 CatchPanicLayer 捕获。中间件 panic 和 handler panic 在 axum 眼里一样——都被 CatchPanic 统一处理。

这意味着:from_fn 中间件的错误模型等同于 handler——Result<T, E: IntoResponse> + ?、没 panic 就不会断连。正是这层一致性让中间件代码读起来和 handler 一样熟悉。

async fn auth(req: Request, next: Next) -> Result<Response, AppError> {
    let user = verify(&req)?;           // ? 把 AppError 短路成 Response
    let response = next.run(req).await;
    Ok(response)
}

AppError: IntoResponse 就行,其他都一样。

实战四:AI 应用的 middleware 模式

LLM 应用里典型的 middleware 需求:

一、API key 认证 + 用户上下文:

async fn auth_and_enrich(
    State(auth): State<AuthService>,
    headers: HeaderMap,
    mut request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let key = headers
        .get("x-api-key")
        .and_then(|v| v.to_str().ok())
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let user_ctx = auth.verify(key).await.map_err(|_| StatusCode::UNAUTHORIZED)?;

    // 注入用户上下文到 extensions, handler 可提取
    request.extensions_mut().insert(user_ctx);

    Ok(next.run(request).await)
}

二、Token 配额预检查:

async fn check_quota(
    State(quota): State<QuotaService>,
    Extension(user): Extension<UserContext>,
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    if !quota.has_quota(user.id, user.tier).await {
        return Err(StatusCode::TOO_MANY_REQUESTS);
    }
    Ok(next.run(request).await)
}

注意这里用 Extension<UserContext>——依赖上一个 auth_and_enrich 已经塞进 extensions。route_layer 的顺序要保证 auth 在 quota 之外(执行时 auth 先跑、quota 后跑)。

三、计费后处理:handler 执行后读取 token 使用量写账单。这不是 from_fn 的典型 pattern,因为需要 "handler 完成后异步上报"——建议 fire-and-forget:

async fn billing_record(
    State(billing): State<BillingService>,
    Extension(user): Extension<UserContext>,
    request: Request,
    next: Next,
) -> Response {
    let response = next.run(request).await;

    // 从响应 extensions 读 token 用量(handler 返回时设置)
    if let Some(usage) = response.extensions().get::<TokenUsage>() {
        let billing = billing.clone();
        let user_id = user.id;
        let usage = *usage;
        tokio::spawn(async move {
            billing.record(user_id, usage).await;
        });
    }

    response
}

关键:billing 的写入用 tokio::spawn 异步执行——不阻塞响应返回给客户端。如果在同步路径上等 billing 写完再返响应,客户端延迟会被拖慢。fire-and-forget 在 LLM 场景常用——token 用量统计、审计日志、通知等都适用。

这三个中间件叠加是 LLM API 的标准栈——/chat 这样的 endpoint 会自动获得"认证 → 配额 → 计费"的完整处理流程,每个环节都是独立的 from_fn,可单独测试和替换。

实战五:请求 ID 追踪

跨服务 tracing 需要 "request id"——每个请求打上唯一 ID,log / downstream call 都带上,方便事后定位。这是 from_fn 的经典用例:

use uuid::Uuid;

async fn request_id(mut request: Request, next: Next) -> Response {
    // 如果 request 已带 id(来自上游 gateway),用它;否则生成新的
    let id = request
        .headers()
        .get("x-request-id")
        .and_then(|v| v.to_str().ok())
        .map(String::from)
        .unwrap_or_else(|| Uuid::new_v4().to_string());

    // 塞进 request extensions,handler 能提取
    request.extensions_mut().insert(RequestId(id.clone()));

    // 打开 tracing span, 让这条 request 的所有 log 都带 request_id
    let span = tracing::info_span!("request", request_id = %id);
    let mut response = async move { next.run(request).await }
        .instrument(span)
        .await;

    // 把 id 写入响应 header, 客户端调试时能看到
    response.headers_mut().insert(
        "x-request-id",
        HeaderValue::from_str(&id).unwrap(),
    );

    response
}

#[derive(Clone, Debug)]
pub struct RequestId(pub String);

几个工程考虑:

  1. 复用上游的 id:如果请求已经从负载均衡/网关过来带了 X-Request-Id,保留它——让整条链路一致
  2. 塞 Extension:handler 里 Extension<RequestId> 能取到,用于业务日志
  3. set span:.instrument(span) 让 handler 和下游 middleware 的 tracing event 都自动带上 request_id field——不用每条 log 手动写
  4. 响应也返回:客户端在调试时能看到 id——bug 报告里带上就能服务端精确定位那条请求

这种中间件是生产 axum 应用的最低标配——和 TraceLayer 配合几乎能满足所有可观测性需求。

测试 from_fn 中间件

from_fn 中间件的单元测试有两种层次。

一、集成测试:挂到 Router 里端到端跑:

#[tokio::test]
async fn auth_returns_401_without_token() {
    use axum::{body::Body, http::Request};
    use tower::ServiceExt;

    let state = AppState::default();
    let app = Router::new()
        .route("/protected", get(|| async { "ok" }))
        .route_layer(middleware::from_fn_with_state(state.clone(), auth))
        .with_state(state);

    let res = app
        .oneshot(Request::builder().uri("/protected").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}

优点:测试完整链路;缺点:要构造整个 Router 状态,测试大而臃肿。

二、单元测试:直接调用中间件函数(只在 axum 仓库内部可行,因为 Next 的 inner 字段是私有的、也没有公开构造函数):

#[tokio::test]
async fn auth_inserts_user_into_extensions() {
    use tower::util::BoxCloneSyncService;

    let state = AppState::default();
    let request = Request::builder()
        .header("authorization", "Bearer valid_token")
        .body(Body::empty())
        .unwrap();

    // 构造一个 Next 指向"直接返回 200 + extensions"的 inner Service
    let inner = tower::service_fn(|req: Request| async move {
        // 把 req.extensions::<User> 的存在作为 assert
        let has_user = req.extensions().get::<User>().is_some();
        assert!(has_user);
        Ok::<_, Infallible>(Response::new(Body::from("ok")))
    });

    let next = Next { inner: BoxCloneSyncService::new(inner) };
    let headers = HeaderMap::new();  // 实际从 request 拿

    let result = auth(State(state), headers, request, next).await;
    assert!(result.is_ok());
}

直接构造 Next 调用函数最单元化,但 pub struct Next { inner: ... } 的字段没有 pub,也没有 Next::new——应用代码里写不出这段。

三、应用侧真正可行的做法:给 FromFnLayer 喂一个假的 inner Service。from_fn(f) / from_fn_with_state(state, f) 返回的是一个 Layer,把它 layer() 到一个 tower::service_fn 上,就得到一个可以直接 oneshot 的 Service——既不用整个 Router,也不用碰私有的 Next:

use axum::{body::Body, extract::Request, http::StatusCode, middleware::from_fn_with_state, response::Response};
use std::convert::Infallible;
use tower::{service_fn, Layer, ServiceExt};

// Future: Send + 'static 必须写出来:impl Trait 会把关联类型 Future 藏起来,
// 不写的话 FromFn 的 `I::Future: Send` 约束无法证明,FromFn 就不是 Service
fn stub_inner() -> impl tower::Service<Request, Response = Response, Error = Infallible, Future: Send + 'static>
       + Clone + Send + Sync + 'static {
    service_fn(|_req: Request| async move {
        Ok::<_, Infallible>(Response::new(Body::from("inner reached")))
    })
}

#[tokio::test]
async fn auth_rejects_without_token() {
    // from_fn_with_state(...) 是 Layer,layer(inner) 得到 FromFn Service
    // (auth 用了 State<AppState>,所以不能用 from_fn——它的 state 是 ())
    let svc = from_fn_with_state(AppState::default(), auth).layer(stub_inner());

    let res = svc
        .oneshot(Request::builder().body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}

stub_inner 返回一个固定响应,中间件短路时根本不会走到它——想断言"中间件放行了",就检查响应体是不是 inner reached。把这个 helper 放进项目的 test_helpers,所有 from_fn 中间件的测试都能复用。

调试 from_fn 中间件

写中间件常遇到的几种调试场景。

一、断言 middleware 真的挂上了:给 middleware 体内加 tracing::debug! 或 eprintln!,发一次测试请求看日志有没有这条 message。没有就说明 middleware 没被调用——检查 route_layer vs layer、检查 Layer 顺序。

二、中间件 panic 但看不到错误:panic 发生在 async future 里,不带 stack trace 直接显示。生产里要配 CatchPanicLayer + panic = "unwind"。调试时 RUST_BACKTRACE=1 能让 stack trace 显示——但 async 的 stack trace 可读性一般。

三、next.run 卡住不返回:通常是 handler 本身慢——middleware 本身很少有长时间操作。用 tokio-console 或 tracing::debug! 前后定位。

四、state 类型不匹配编译错:错误消息只会说 the trait bound `FromFn<...>: Service<...>` is not satisfied(实测),不会直接指出 State 不对——检查中间件函数里 State<T> 的 T 能否从 from_fn_with_state 给的 state 类型经 FromRef 得到。给中间件函数加 #[axum::debug_middleware](需开 macros feature)能得到更具体的报错。

五、"Future cannot be sent between threads" 编译错:通常是中间件体内跨 await 持有了 !Send 的东西——std::sync::MutexGuard、Rc<T>、&RefCell<T>(RefCell 不是 Sync)。换成 tokio::sync::Mutex 或在 await 前 drop guard。

Next 自己也是 Service

axum/src/middleware/from_fn.rs:352-364:

impl Service<Request> for Next {
    type Response = Response;
    type Error = Infallible;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

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

    fn call(&mut self, req: Request) -> Self::Future {
        self.inner.call(req)
    }
}

Next 实现 Service——意味着它能作为 Tower Service 使用。这让 middleware 函数里能用其他 Tower 组合工具:

async fn retry_middleware(request: Request, next: Next) -> Response {
    use tower::ServiceExt;
    let (parts, body) = request.into_parts();

    // 注意: 不能直接 retry——next 可以 clone,但 body 只能消费一次,每次都得重建 request
    // 但可以对 next 做 Tower 的其他操作
    let mut next_service = next;  // Next 是 Service
    // ...
}

实际 Next::run 更简单好用——大多数场景用 run 就够。Service impl 主要是为了让 Next 能 compose 进 Tower 生态(比如 next.map_response(...)——但 axum 自己没大量用这个)。

为什么 Next::run 按值消费 self

一个值得注意的设计细节:Next::run(self, req) 按值消费 self,不是 &mut self。这意味着:

  • 一个 Next 只能 run 一次:调用后 self 被消耗、不能重用
  • 中间件不能重试:想"失败了再调一次 next"——需要先 clone Next

这个设计限制匹配了 Service::call 的语义——call 消费 readiness token。Next 内部就是一个 Service,让 run 按值就是明确"这次 call 用掉了 readiness"。如果要重试,要先 Next::clone(Next 实现 Clone)、然后两个独立 Next 各 run 一次——但每次都要重新构造 request(body 消费过了)。

这种设计让 run 的语义直白——用户看 next.run(req).await 就知道 next 不能再用了,不会误操作。

性能调优建议

中间件栈的性能有几个常见关注点:

一、避免每请求都 clone 大对象:中间件函数是 FnMut + Clone——每次请求 clone 一份。如果你捕获了一个大数据结构,clone 可能贵。解决:用 Arc<T> 包装大数据,clone 只剩一次引用计数自增。

二、避免跨 await 持有锁:std::sync::MutexGuard 不是 Send,持有时 future 变成 !Send,编译失败。即使用 tokio::sync::Mutex(可以跨 await),长时间持有也是性能问题——所有请求被串行化。lock().await 前把要做的活准备好、拿到锁后快速完成、立即 drop。

三、减少每请求的分配:from_fn 本身每请求有三次 box 分配(中间件 future 的 Box::pin、BoxCloneSyncService::new、next.run 时内层 future 的 Box::pin)——这是必然开销。额外的分配(format! / to_string / Vec::new)尽量合并或用 Cow。

四、路由 layer 而不是全局 layer:认证 layer 如果只有部分 endpoint 需要,把这些路由放在 route_layer 之前、其余路由放在它之后(route_layer 只包住调用前已注册的路由)——非认证 endpoint 不走这个 middleware、省一段开销。

五、精确监控每个中间件的耗时:给中间件函数加 #[tracing::instrument],它会为这个 async 函数开一个 span、记录进入与退出;再接一个能消费 span 时长的 subscriber(tracing-opentelemetry、或自己写的 timing layer),就能把每层中间件的耗时分布导出去。定位性能问题时这个维度很关键。

中间件栈的可视化

最后把一个完整生产 Router 的中间件栈画出来:

flowchart TB
    REQ[HTTP Request] --> OUT
    OUT[TraceLayer<br/>打开 tracing span] --> PANIC
    PANIC[CatchPanicLayer<br/>捕获 panic] --> HE
    HE[HandleErrorLayer<br/>Timeout/Overload 转响应] --> TIMEOUT
    TIMEOUT[tower TimeoutLayer<br/>30s 超时] --> LOG[log_request<br/>自定义 from_fn]
    LOG --> CORS[CorsLayer<br/>Tower]
    CORS --> COMP[CompressionLayer<br/>Tower]
    COMP --> RATE[rate_limit<br/>自定义 from_fn]
    RATE --> AUTH[auth<br/>自定义 from_fn]
    AUTH --> HANDLER[Handler]
    HANDLER --> AUTH
    AUTH --> RATE
    RATE --> COMP
    COMP --> CORS
    CORS --> LOG
    LOG --> TIMEOUT
    TIMEOUT --> HE
    HE --> PANIC
    PANIC --> OUT
    OUT --> RES[HTTP Response]
    style AUTH fill:#fef
    style RATE fill:#fef
    style LOG fill:#fef
    style OUT fill:#efe
    style CORS fill:#efe
    style COMP fill:#efe
    style TIMEOUT fill:#efe
    style HE fill:#fee
    style PANIC fill:#fee

三种颜色区分三类中间件:

  • 绿色(Tower 生态):tower / tower-http 提供,开箱即用
  • 粉色(axum from_fn):自己写的业务中间件
  • 红色(错误处理):CatchPanic + HandleError,保证响应总产生

这种栈是 axum 生产最常见的形态——10 层左右的 Layer,每层职责清晰,开发者看配置就知道请求会经过什么流程。注意 HandleErrorLayer 画在 TimeoutLayer 外面——只有这样它才接得住 tower::timeout::Timeout 抛出的 BoxError(第 12 章)。

常见陷阱

陷阱一:middleware state 和 Router state 类型不一致:

// ❌ from_fn_with_state 给的 state 和 Router::with_state 给的不一致
let app = Router::new()
    .route("/", get(handler))
    .route_layer(middleware::from_fn_with_state(middleware_state, mw))  // MiddlewareState
    .with_state(app_state);  // AppState

// handler 里 State<AppState> 是从 app_state 来
// middleware 里 State<MiddlewareState> 是从 middleware_state 来

两个 state 是完全独立的——一个给中间件、一个给 handler。但这容易造成混乱:想让 middleware 和 handler 共享 state 时必须传同一个实例(或从同一个派生)。

推荐模式:middleware 的 state 就是 Router state 的一部分——用 FromRef 或就是 clone 一份:

let shared = AppState { /* ... */ };
let app = Router::new()
    .route("/", get(handler))
    .route_layer(middleware::from_fn_with_state(shared.clone(), mw))
    .with_state(shared);

陷阱二:body 消费后不能再 next.run:

// ❌ 消费了 body 就不能 next.run(原 request)
async fn wrong(mut request: Request, next: Next) -> Response {
    let bytes = Bytes::from_request(request, &()).await.unwrap();  // 消费 body
    // request 已经被 move, 不能再用
    // next.run(???)  <- 没 request 可用了
    next.run(/* ??? */).await
}

如果想 peek body 然后继续——必须先 buffer、读完再重构 request:

async fn peek(request: Request, next: Next) -> Response {
    let (parts, body) = request.into_parts();
    let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
    // 用 bytes 做检查
    let request = Request::from_parts(parts, Body::from(bytes));  // 重构
    next.run(request).await
}

注意这让 body 整个被加载到内存——对大 body(文件上传)不合适。

陷阱三:next.run 忘了 .await:

// ❌ 忘了 await, 返回的是 Future 不是 Response
async fn bad(req: Request, next: Next) -> Response {
    next.run(req)  // Future<Output = Response>
        // 忘了 .await, 类型错
}

编译会失败——但错误消息可能不直观(提示类型不匹配)。一般都能快速察觉。

陷阱四:route_layer 和 layer 搞混:route_layer 只包调用它之前已注册的路由、layer 包调用时已注册的路由外加 fallback。认证中间件通常用 route_layer——避免 fallback 404 也需要认证(主线上若先设了 .fallback() 再 route_layer,fallback 也会被包住,见实战一)。CORS / trace 通常用 layer 覆盖全部。

陷阱五:middleware 里 spawn task 但不等待:

async fn mw(req: Request, next: Next) -> Response {
    tokio::spawn(async { something().await; });  // fire-and-forget 不 await
    next.run(req).await
}

fire-and-forget 可以——但如果 spawn 的 task panic,log 里只看到 "task panicked" 没有 request 上下文。建议 spawn 内用 tracing::error! 带上足够上下文。

一个微妙点:FnMut 而非 FnOnce

FromFn 的 Service impl 要求 F: FnMut(...) -> Fut + Clone——不是 FnOnce。有人会担心:middleware 函数体里 next.run(req).await 按值消费了 next,这不就成了 FnOnce?其实不会——按值消费的是参数,每次调用都传入一个新的 next;闭包是 Fn / FnMut / FnOnce 中的哪一种,取决于它怎么用捕获的状态。async fn 没有捕获,天然实现 Fn。那为什么还要 Clone?

关键在 self.f.clone():

// 简化源码
fn call(&mut self, req: Request) -> Self::Future {
    let mut f = self.f.clone();  // clone 一份 f 给本次 call 用
    // ...
    let future = Box::pin(async move {
        // ...
        f(T1, T2, T_last, next).await.into_response()
    });
    ResponseFuture { inner: future }
}

每次 call 都 clone 一份 f,把副本 move 进 'static 的 async block——这个 future 要独立于 &mut self 存活,不能借用 self.f。FnMut + Clone 组合起来就是"每次 call 拿一份自己的 f、在副本上调用一次"。这个模式在 Rust async 生态里很常见——Tower、axum 都用。

对用户来说:middleware 函数里可以自由 next.run(req).await(消费)、可以捕获值 move 进 async block——只要函数本身 Clone(通常 async fn 都自动 Clone,除非捕获了 !Clone 类型)。

from_fn 的限制

几个 from_fn 的边界:

一、无 poll_ready 钩子:middleware 函数是 async fn,poll_ready 直接转发 inner——不能自己做 back-pressure。如果中间件本身是资源消耗大(比如需要数据库连接),这个限制可能成问题——解决方法是在函数体内 .await 获取资源(但这会让 poll_ready 失去 back-pressure 语义)

二、分配开销:每个请求两次 Box::pin(中间件 future、next.run 时的内层 future)和一次 BoxCloneSyncService::new,三次堆分配。热路径场景(极低延迟网关)可能不想付这个代价

三、不能 generic over Service trait:middleware 函数签名里 Next 是具体类型,请求、响应固定为 axum 的 Request / Response,内层 Service 的 Error 必须是 Infallible。FromFnLayer 本身仍是标准 Tower Layer(前面测试就把它 layer 到了 service_fn 上),但出了 axum 的类型体系(比如 tonic 的 gRPC 栈、Error 不是 Infallible 的 Service)就贴不上

四、FnMut 但不能捕获 &mut state:函数要 Clone + Send + 'static——不能捕获非 Clone 或非静态的东西。state 要通过 from_fn_with_state 传入

大多数业务场景这些限制无关紧要——但如果碰到,说明场景超出了 from_fn 的设计初衷,需要回到原生 Tower Layer。

from_fn 与 Tower 原生 Layer 的性能对比

维度 from_fn 原生 Tower Layer
每请求额外分配 2 × Box::pin + 1 × BoxCloneSyncService::new 0(如果 Service 写对)
vtable 调用 Next::inner.call 一次(通过 trait object),外加每次 poll 装箱 future 的动态派发 0(编译期单态化)
代码量(简单 case) 一个 async fn Layer + Service + Future 三个类型
泛型参数数 FromFn<F, S, I, T> 四个 自己定,通常两三个
调试信息 函数名在 Debug 里以 type_name::<F>() 打印 需要给自己的类型手写 Debug

差异集中在"每请求三次堆分配 + 动态派发"上——对大多数业务可忽略。生产项目大部分中间件用 from_fn、性能关键的用原生 Layer,这是合理的工程分工。

中间件对 body 的处理

一个经常被忽略的细节:中间件能否修改 body 取决于使用方式。

场景一:不碰 body,纯透传 — 默认情况,最好:

async fn mw(req: Request, next: Next) -> Response {
    // req 按 FromRequest 的恒等 impl 拿到,不触发 body 消费
    next.run(req).await
}

场景二:读完 body 再继续 — 要消费再重构:

async fn log_body(req: Request, next: Next) -> Result<Response, StatusCode> {
    let (parts, body) = req.into_parts();
    let bytes = axum::body::to_bytes(body, 1024 * 1024).await
        .map_err(|_| StatusCode::PAYLOAD_TOO_LARGE)?;
    tracing::info!(size = bytes.len(), "request body");
    let req = Request::from_parts(parts, Body::from(bytes));
    Ok(next.run(req).await)
}

场景三:只想 peek 头几个字节 — axum 没有现成的 peek 工具:要么读完,要么自己用 http_body_util::BodyExt::frame 读出前几帧,再把已读的帧和剩下的 body 拼成一个新 Body(Body::from_stream)交给 next,写起来明显更繁琐。

第二种模式让 body 整个被缓冲到内存,不适合大 body。如果要做 body 相关处理(签名验证、内容检查),考虑:

  • 限制 body 最大(DefaultBodyLimit + 中间件里的 to_bytes(body, LIMIT))
  • 文件上传等场景走特殊 route,没签名验证,或者签名验证 handler 自己做

from_fn 与 Handler 的类型关系

深入看会发现 from_fn 的 middleware 函数和 handler 在类型上几乎对称:

维度 Handler from_fn middleware
签名形式 async fn(提取器...) -> T: IntoResponse async fn(提取器..., Next) -> T: IntoResponse
提取器约束 前 N-1 个 FromRequestParts + 最后 FromRequest 前 N-1 个 FromRequestParts + 最后 FromRequest + Next
返回类型 IntoResponse IntoResponse
Error 契约 Result<T, E: IntoResponse> Result<T, E: IntoResponse>
宏展开参数数 0 个,或 1-16 个提取器(impl Handler<((),), S> 加 all_the_tuples!) 1-16 个提取器(只有 all_the_tuples!,最后一个必须是 FromRequest),再加 Next
state 获取 State<S> 从 .with_state 拿 State<S> 从 from_fn_with_state 拿

区别只在末尾的 Next——其他维度几乎一模一样。这不是巧合——axum 团队有意让 middleware 函数的心智成本等于"handler + 一个 Next 参数",学会了 handler 就等于学会了 middleware。

从源码上看,FromFn::call 的实现和 HandlerService::call 的实现几乎平行——都是"提取参数 → 调用函数 → IntoResponse 转换"。差异只在 FromFn 多了一步"把 inner Service 包成 Next 塞给函数"。

这种设计一致性让 axum 的学习曲线相对平滑:不需要学两套不同的函数签名规则,一套"提取器 + async fn"的模型适用于 handler 和 middleware。

跨专栏关联:Tower 生态的 Layer 与 axum from_fn

Tower 原生 Layer 是整个 Rust 异步服务生态的通用抽象——用在 axum、tonic(gRPC)、数据库连接池、HTTP 客户端等。from_fn 是 axum 为 HTTP 中间件场景特制的"便利封装"——牺牲了通用性换来语法简洁。

两者关系:Layer 是基石、from_fn 是便利层。from_fn 本质上就是生成一个 Layer 实现——FromFnLayer + FromFn Service。写 from_fn 的中间件最终还是 Tower Layer——只是源码写起来不用手写 Layer/Service。

《Hyper 与 Tower:工业级 HTTP 栈》专栏第 3 章详细讨论了 Layer + Service 的设计——包括为什么 Layer 是 "taking ownership of inner Service" 的模式、poll_ready 在 Layer 组合下怎么传播(poll_ready/call 为什么分离见该专栏第 2、4 章)。读那一章后再看 FromFn 的 Service impl,会发现它是最典型的 Tower Layer 模式——只是内部把逻辑委托给了用户的 async 函数。

axum 和 Tower Layer 的混用

from_fn 不等于全部——和 Tower 生态的 Layer 一起用是常态:

use axum::http::StatusCode;
use std::time::Duration;
use tower_http::{trace::TraceLayer, timeout::TimeoutLayer, cors::CorsLayer};

let app = Router::new()
    .route("/protected", get(protected))
    .route_layer(middleware::from_fn_with_state(state.clone(), auth))  // 自定义 from_fn,只包 /protected
    .route("/", get(handler))                                          // 之后注册,不过 auth
    .layer(TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(30))) // tower-http Layer
    .layer(CorsLayer::permissive())                                      // tower-http Layer
    .layer(TraceLayer::new_for_http())                                   // tower-http Layer
    .with_state(state);

这里刻意用的是 tower-http 的 TimeoutLayer,它的 type Error = S::Error,超时后直接返回一个状态码由 with_status_code 指定的空响应(tower-http/src/timeout/service.rs;tower-http 0.6.7 起不带状态码的 TimeoutLayer::new 已标 deprecated,它默认给 408 Request Timeout),所以不需要配 HandleErrorLayer。只有换成 tower::timeout::TimeoutLayer(Error = BoxError)时才必须在它外面加一层 HandleErrorLayer——两个同名类型的语义差别是 axum 用户最容易踩的坑之一。

执行顺序还是遵循洋葱——越靠后 layer 的越外层、先看到请求。Tower Layer 和 from_fn 没有本质区别——它们在 Layer 栈里一视同仁。差别只在代码怎么写。

何时选 Tower Layer、何时选 from_fn

需求 Tower Layer from_fn
生态标准方案已存在(compression、tracing、cors、timeout) ✓ 直接用 自己写 from_fn 重复造轮子
业务特定的认证 / rate limit / 计费 复杂 ✓ 自然写
希望发布给其他 axum / tonic / 通用 tower 项目用 ✓ 通用 只能 axum
希望 poll_ready 真正做 back-pressure ✓ 不支持
快速原型 繁琐 ✓

实际项目里 tower-http 的几个中间件几乎是标配——TraceLayer 做日志、CorsLayer 做 CORS、TimeoutLayer 做超时、CompressionLayer 做压缩。自己的业务逻辑(认证、配额、计费)写 from_fn。两者混用让项目既享受生态又保留灵活性。

本章总结

from_fn 是 axum 最常用的中间件工具。核心要点:

一、签名和 handler 对称:学了 handler 就会 from_fn——只多一个 Next 参数

二、Next 是剩余栈的抽象:用 BoxCloneSyncService 类型擦除,让 middleware 函数签名固定。next.run(req).await 透传、不调则短路

三、错误模型统一:Result<Response, E: IntoResponse> + ?、Error 是 Infallible——和 handler 一模一样

四、state 通过 from_fn_with_state 注入:不能直接捕获 Router state——这个限制让 from_fn 和 Router 的 state 保持类型解耦

五、性能代价可接受:每请求多三次 box 分配和若干次动态派发——毫秒级业务完全可以忽略

六、不是唯一选择:生态标准方案用 tower-http、业务特定用 from_fn——混用是常态

更深的几个设计原则也值得记住:

一、"洋葱模型"是自然的:Tower 的 Layer 叠加 + from_fn 的 next.run 前后代码,自然形成 before/after 对称的洋葱——不需要框架特殊配置

二、类型驱动的 middleware 栈:签名里 State<S>、提取器、Next 都是类型级约束——编译期就能保证 middleware 栈自洽

三、错误路径永远回归 Response:从 handler 到 middleware 到框架,所有 Err 都必须能 IntoResponse——这条"全链路 Infallible"让生产稳定性有 foundation

下一章继续讲另外三种中间件 helper——map_request / map_response / from_extractor。它们是 from_fn 和原生 Layer 之间的中间形态——适合"只要改 request"、"只要改 response"、"只要在 handler 前做提取器校验"这三种更窄的场景。每种都有自己的 API 简化点。理解了 from_fn 之后,下一章的几个 helper 会感觉像是"针对特定场景的进一步简化"——核心心智模型不变。