Skip to content

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

前四章讲完请求到响应的完整链路——提取器、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"的中间件大概这样:

rust
// 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 让同样的工作变成:

rust
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 的 F 要求是 FnMut + Clone + Send + 'static——几乎所有 async fn 都自动满足。但如果函数闭包捕获了某些类型可能不满足:

  • 捕获 !Send 类型(如 Rc<T>)→ 编译失败。解决:用 Arc<T> 替换
  • 捕获 !Clone 类型(如 TcpStream)→ 函数不 Clone、from_fn 拒绝
  • 捕获非 'static 引用(如本地变量的 &str)→ 'static bound 失败。解决:move 一份 own 的 String / Arc<str>

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

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

一个合理的反问:Tower 能不能让 Layer 本身就接受 async fn?答案是不能——tower::Layer 是一个通用库,面对各种非 async 场景(比如同步的 Service),无法直接支持 async function 作为一等公民。

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

一、固定 Request/Response 类型:from_fn 写死了 RequestResponse(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

rust
// axum/src/middleware/from_fn.rs:336-350
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,只能调一次。真的要重试需要其他机制

BoxCloneSyncService:为什么需要类型擦除

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

因为 Next 要能被 async 函数按值消费async fn middleware(req, next: Next) 里 next 的类型必须是具体的——不能是 <S: Service<...>> 这样的泛型(async 函数的泛型参数不能 late-bound)。

类型擦除让 Next 有一个确定的具体类型——代价是一次虚函数调用 + 一次堆分配(box)。在中间件场景下这层开销可以忽略——中间件本身不在纳秒级热路径上。

BoxCloneSyncServiceBoxService 多要求 Clone + Sync——axum 需要 Service 能被多个请求共享(因为一个 Router 被多个连接并发使用)。Tower 提供这个变体专门为 axum 这类场景。

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

from_fn 文档(from_fn.rs:21-32)明确列出签名约束:

  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 能透传给下一层。

合法签名举例:

rust
// 最简:只有 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(req 是 FromRequest<S>)
async fn m4(method: Method, req: Request, next: Next) -> Response;  // req 消费 body

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

FromFn 的 Service 实现

中间件函数经过 from_fn(f) 变成 FromFnLayer<F, S, T>layer(inner) 生成 FromFn<F, S, I, T>——最终的 Service。核心 impl 在 from_fn.rs:254-318(宏展开 1-16 个前置参数版本):

rust
// 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 的结构

from_fn.rs:367-377

rust
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 的内部数据流:

from_fn_with_state:注入共享 state

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

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

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

两种风格的适用场景:

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

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

from_fn 的开销:每次请求一次 BoxCloneSyncService::new + 一次 Box::pin——几十纳秒到一百纳秒。对 millisecond 级的 handler 业务完全可忽略;对 sub-microsecond 热路径可能显著。

中间件的叠加与执行顺序

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

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

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

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

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

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

顺序在生产里的影响

  • 认证在最外:请求没认证通过就别让其他中间件浪费 CPU
  • 日志在最外或最外之次:记录完整生命周期的时间
  • rate limit 在认证之后:rate limit 通常按用户/API key 计数,需要先认证拿到身份
  • 压缩(compression)放内:压缩后的数据经过的中间件越少越好

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

实战一:认证中间件

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

rust
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 传递类型化数据。第 11 章讲过这种模式。

三、route_layer vs layerroute_layer 只作用到指定路由、不影响 fallback / nest 等路由;layer 作用于整个 Router 包括 fallback。认证通常用 route_layer——让 fallback(比如 404 页面)不被认证拦截。

实战二:请求日志中间件

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

rust
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 propagation、和 OpenTelemetry 集成等高级功能。生产里建议:轻量自定义用 from_fn,完整可观测性用 TraceLayer + 自定义 span 配置。

实战三:基于 state 的 rate limit

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

rust
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(包含锁),跨 await 持有会让 future 非 Send(编译失败)。显式 drop 在 await 前让锁释放

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

这种 sliding window rate limit 仅是示例,真实 production 推荐用 tower_governor / tower::load_shed 或配合 Redis 做分布式 rate limit。

分布式场景的 rate limit

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

rust
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 非原子、race condition 时 TTL 可能永远不设上——推荐写一个 Lua script 原子处理
  2. 失败路径行为:Redis 宕掉时 middleware 是拒绝(安全优先)还是放行(可用性优先)?生产需要明确策略。默认建议放行——否则 Redis 单点故障让整个服务 429
  3. 连接池:middleware 每次用 Redis,连接池大小要够。bb8-redis / deadpool-redis 是常见选择
  4. 本地 + 分布式混合:结合本地(DashMap)做粗限(防爆发流量让 Redis)+ 分布式(Redis)做精限——混合方案更稳

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

from_fn 的历史演进

from_fn 在 axum 不同版本的演化:

  • axum 0.3 之前:没有 from_fn。所有中间件都要手写 Layer/Service——门槛高,劝退很多想自定义中间件的用户
  • axum 0.3:引入 middleware::from_fn——最早版本只支持单一 (req, next) 签名,不支持提取器
  • axum 0.5+:加入提取器支持——signature 可以带 1-16 个 FromRequestParts + 一个 FromRequest。让 middleware 能用 Method / HeaderMap 这些 handler 级别的便利
  • axum 0.6from_fn_with_state 加入——让 middleware 能访问 Router state
  • axum 0.7+:稳定、小改进(类型 bound 简化、错误消息改善)

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

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

javascript
// Koa
app.use(async (ctx, next) => {
    // before
    await next();
    // after
});
rust
// 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 等场景:

rust
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 自己设的):

rust
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
}

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

rust
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(比如在 body 前加 header 签名):

rust
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::layer 配合 cfg 或者用 MapOr 之类的组合子。from_fn 内部条件更适合"每个请求的快速判断",不适合"全局 on/off"。

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

rust
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——比串行快一半。做多个独立后端调用时(比如同时查用户表和权限表)值得用。但要注意 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) -> ResponseOPTIONS 请求短路处理
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 不会经过 middle_mw 的"after"代码——洋葱被中途截断。

二、中间件的 Err 也能经过 HandleErrorLayer:但只能处理 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 一样熟悉。

rust
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 认证 + 用户上下文

rust
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 配额预检查

rust
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:

rust
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 的经典用例:

rust
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 里端到端跑

rust
#[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 状态,测试大而臃肿。

二、单元测试:直接调用中间件函数

rust
#[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 调用函数——更快、更单元化。缺点:要 import Next 的构造器(axum 里它是 private),这种测试通常只在 axum 自己的代码里做。应用侧多用集成测试。

三、用 mock_next pattern

rust
// 自己写个 helper 模拟 Next
fn mock_next(response: Response) -> Next {
    let inner = tower::service_fn(move |_: Request| {
        let response = response.clone();
        async move { Ok::<_, Infallible>(response) }
    });
    Next { inner: BoxCloneSyncService::new(inner) }
}

把这个 helper 抽成测试工具——后续所有中间件测试都可以用 mock_next 生成一个"确定响应"的 Next 去测中间件行为。推荐项目里放一个 test_helpers::mock_next,所有 middleware 的测试都基于它。

调试 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-consoletracing::debug! 前后定位。

四、state 类型不匹配编译错:错误消息里expected State<A>, found State<B>——检查 from_fn_with_state 的 state 类型和 .with_state 的一致。

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

Next 自己也是 Service

from_fn.rs:352-364

rust
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 组合工具:

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

    // 注意: 不能真的 retry, 因为 next 只能被 consume 一次、body 也只能消费一次
    // 但可以对 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 只是 Arc::clone 零成本。

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

三、减少每请求的分配:from_fn 本身每请求有两次 box 分配(Box::pin + BoxCloneSyncService::new)——这是必然开销。额外的分配(format! / to_string / Vec::new)尽量合并或用 Cow

四、路由 layer 而不是全局 layer:认证 layer 如果只有部分 endpoint 需要,用 route_layer 限制作用域——非认证 endpoint 不走这个 middleware、省一段开销。

五、精确监控每个中间件的耗时tracing#[instrument] 能自动计时 async 函数——加到中间件函数上,就能在日志里看到每个中间件的 p50 / p99。生产里这个指标是定位性能问题的关键。

中间件栈的可视化

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

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

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

这种栈是 axum 生产最常见的形态——10 层左右的 Layer,每层职责清晰,开发者看配置就知道请求会经过什么流程。

常见陷阱

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

rust
// ❌ 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 一份:

rust
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

rust
// ❌ 消费了 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:

rust
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

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

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

陷阱四:route_layer 和 layer 搞混route_layer 只包路由、layer 包整个 Router(包括 fallback)。认证中间件通常用 route_layer——避免 fallback 404 也需要认证。CORS / trace 通常用 layer 覆盖全部。

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

rust
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::call 的 bound 是 F: FnMut(...) -> Fut + Clone——不是 FnOnce。但 middleware 函数体里通常用 next.run(req).await——run 按值消费 next——这是 FnOnce 的消费 pattern。两者如何共存?

关键在 self.f.clone()

rust
// 简化源码
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——clone 的副本在本次 future 里被 FnOnce 式消费。FnMut + Clone 组合起来实际上是"每次 call 独立、可以消费内部状态、但下次 call 能拿新副本"。这个模式在 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::pinBoxCloneSyncService::new——大概 100 ns。热路径场景(极低延迟网关)可能不想付这个代价

三、不能 generic over Service trait:middleware 函数签名里 Next 是具体类型,不能像 Tower Layer 那样被任意 Service 继承。这让 from_fn 中间件不能"到处贴"——只在 axum 的 Router 里用

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

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

from_fn 与 Tower 原生 Layer 的性能对比

维度from_fn原生 Tower Layer
每请求额外分配1 × Box::pin + 1 × BoxCloneSyncService::new0(如果 Service 写对)
vtable 调用1 次(Next::inner.call 通过 trait object)0(编译期单态化)
代码行数(简单 case)~10 行~40 行
泛型参数数1 个(T - 提取器 tuple)通常 ~3-5 个
调试信息函数名可见所有类型需要 Debug

性能差异在微秒量级——对大多数业务可忽略。生产项目大部分中间件用 from_fn、性能关键的用原生 Layer——这种"90-10 分工"是合理工程选择。

中间件对 body 的处理

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

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

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

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

rust
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 头几个字节 — 不读完整 body,但没有 stream peek 机制——只能读完 / 不读二选一。

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

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

from_fn 与 Handler 的类型关系

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

维度Handlerfrom_fn middleware
签名形式async fn(提取器...) -> T: IntoResponseasync fn(提取器..., Next) -> T: IntoResponse
提取器约束前 N-1 个 FromRequestParts + 最后 FromRequest前 N-1 个 FromRequestParts + 最后 FromRequest + Next
返回类型IntoResponseIntoResponse
Error 契约Result<T, E: IntoResponse>Result<T, E: IntoResponse>
宏展开参数数0-16 个提取器0-15 个提取器(加 Next 共 16)
state 获取State<S>.with_stateState<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" 的模式、为什么 Service 的 poll_ready/call 分离。读那一章后再看 FromFn 的 Service impl,会发现它是最典型的 Tower Layer 模式——只是内部把逻辑委托给了用户的 async 函数。

axum 和 Tower Layer 的混用

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

rust
use tower_http::{trace::TraceLayer, timeout::TimeoutLayer, cors::CorsLayer};

let app = Router::new()
    .route("/", get(handler))
    .route("/protected", get(protected))
    .route_layer(middleware::from_fn_with_state(state.clone(), auth))  // 自定义 from_fn
    .layer(TraceLayer::new_for_http())                                   // Tower Layer
    .layer(CorsLayer::permissive())                                      // Tower Layer
    .layer(TimeoutLayer::new(Duration::from_secs(30)))                   // Tower Layer
    .layer(HandleErrorLayer::new(timeout_handler))                       // axum 适配
    .with_state(state);

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

何时选 Tower Layer、何时选 from_fn

需求Tower Layerfrom_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 保持类型解耦

五、性能代价可接受:每请求多 100 ns 的 box 分配——millisecond 级业务完全忽略

六、不是唯一选择:生态标准方案用 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 会感觉像是"针对特定场景的进一步简化"——核心心智模型不变。

基于 VitePress 构建