Axum 设计与实现

第14章 map_request / map_response / from_extractor:变换式中间件

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

第 13 章讲的 from_fn 是能力最广的中间件形式——前后钩子、短路、提取器、state——什么都能做。但很多场景只需要更受限的能力:只改 request、只加工 response、只做准入校验。axum 为这些常见场景提供了三个更"专用"的 helper:

  • map_request:只修改 request(或短路拒绝),不接触 response
  • map_response:只加工 response,不干预 request 处理
  • from_extractor:把任何 FromRequestParts 提取器当中间件用——提取成功放行、失败返 rejection

这三个 helper 不是 from_fn 的替代——而是专用化。写起来更直接(不需要 Next)、意图更明显(读代码的人知道中间件只做哪类工作)。

更精确地说,这三个 helper 是面向常见情况的设计——大多数实际中间件只做某一类工作(仅改 request、仅改 response、仅做校验),没必要都用全能的 from_fn。提供专用 API 让常见情况更简洁、特殊情况回退到 from_fn 或 Tower Layer。这种"常用场景优先、特殊场景兜底"的 API 设计风格在 axum 各处都见——提取器有 OptionalFromRequest、响应有 NoContent、中间件有本章讨论的三兄弟。

三者的职责对比

先放一张总览图说明三者在请求处理流程里的位置:

flowchart LR
    classDef req fill:#efe,stroke:#080
    classDef res fill:#fef,stroke:#808
    classDef check fill:#eef,stroke:#066
    subgraph "from_fn: 全能"
        FF1["req 进入"] --> FF_PRE["pre 逻辑"] --> FF_NEXT["next.run"] --> FF_POST["post 逻辑"] --> FF2["res 返回"]
    end
    subgraph "map_request: 只动 req"
        MR1["req 进入"] --> MR_PRE["只改 req 或<br/>短路"]:::req --> MR_INNER["inner.call"] --> MR2["res 直接返回"]
    end
    subgraph "map_response: 只动 res"
        MRE1["req 直接进入"] --> MRE_INNER["inner.call"] --> MRE_POST["只改 res"]:::res --> MRE2["res 返回"]
    end
    subgraph "from_extractor: 只校验"
        FE1["req 进入"] --> FE_CHECK["提取器跑一次"]:::check --> FE_OK{"成功?"} --> FE_INNER["inner.call"] --> FE2["res 返回"]
        FE_OK -.失败.-> FE_REJ["rejection"]
    end

三者都比 from_fn 少某种能力:map_request 没有 response 加工能力、map_response 没有 request 加工能力、from_extractor 连处理逻辑都没有——只做"继续还是不继续"的决策。

这种"缩减的能力"换来两个好处:代码更简洁(少写一些 next.run(req).await)和意图更清晰(读的人一眼知道这个中间件干什么、不干什么)。

选哪种是人机工程学问题

从能力上讲,from_fn 能做的事情 map_request/map_response/from_extractor 都能做——只要在 from_fn 里写一样的逻辑、适当透传 next.run 即可。那为什么还要这三个?

答案是读代码的人受益。

考虑两段逻辑等价的代码:

// from_fn 写法
async fn auth(req: Request, next: Next) -> Result<Response, StatusCode> {
    verify(&req)?;
    Ok(next.run(req).await)
}

// map_request 写法
async fn auth<B>(req: Request<B>) -> Result<Request<B>, StatusCode> {
    verify(&req)?;
    Ok(req)
}

两段在行为上完全等价。但 map_request 版本少一个 next.run(req).await——读的人看函数签名就知道"这只改 request、不看 response"。from_fn 版本虽然简单,但签名里带着 Next——读者要读完函数体才能确定作者有没有做 response 后处理。

这层"意图可读性"是专用 helper 存在的真实理由——不是性能、不是功能、而是让读代码的人负担更小。一个项目里每种中间件类型都用合适的 helper,签名即文档——维护时减少"猜作者意图"的时间。

map_request:只改 request

axum/src/middleware/map_request.rs:117-119:

pub fn map_request<F, T>(f: F) -> MapRequestLayer<F, (), T> {
    map_request_with_state((), f)
}

用法:

use axum::{middleware::map_request, http::Request, Router, routing::get};

async fn add_header<B>(mut request: Request<B>) -> Request<B> {
    request.headers_mut().insert("x-foo", "foo".parse().unwrap());
    request
}

let app = Router::new()
    .route("/", get(handler))
    .layer(map_request(add_header));

函数签名是 Request<B> -> Request<B>——输入一个 request、输出一个 request。中间件不看 response——inner service 产出的 response 直接透传。

IntoMapRequestResult:两种合法返回值

axum/src/middleware/map_request.rs:367-386 定义了一个 sealed trait:

// axum/src/middleware/map_request.rs:367-386
pub trait IntoMapRequestResult<B>: private::Sealed<B> {
    fn into_map_request_result(self) -> Result<Request<B>, Response>;
}

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<Self, Response> {
        Ok(self)
    }
}

两种返回类型都合法:

  1. Request<B>:直接返回修改后的 request——总是继续
  2. Result<Request<B>, E> where E: IntoResponse:Ok 继续、Err 短路返响应

第二种让 map_request 能做"校验 + 短路":

async fn auth<B>(request: Request<B>) -> Result<Request<B>, StatusCode> {
    let auth_header = request.headers().get("authorization");
    match auth_header.and_then(|v| v.to_str().ok()) {
        Some(s) if token_is_valid(s) => Ok(request),
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}

Err 变成 Response 短路——和 from_fn 的 return Err(StatusCode::...) 等价。区别只在 from_fn 还能"处理完再加工 response"——map_request 不能。

Service impl:核心逻辑

axum/src/middleware/map_request.rs:247-320 宏展开出 1-16 个提取器参数版本。关键段(call 方法内):

// axum/src/middleware/map_request.rs:287-315 (简化)
let future = Box::pin(async move {
    // 依次提取 T1, T2, ..., Tn, T_last
    let T1 = match T1::from_request_parts(&mut parts, &state).await { ... };
    // ...
    let req = Request::from_parts(parts, body);
    let T_last = match T_last::from_request(req, &state).await { ... };

    // 调用用户函数
    match f(T1, ..., T_last).await.into_map_request_result() {
        Ok(req) => ready_inner.call(req).await.into_response(),
        Err(res) => res,
    }
});

逻辑很清晰:

  1. 依次调用提取器(和 handler / from_fn 一致)
  2. 把提取值传给用户函数 f
  3. 用户函数返回值经 into_map_request_result 统一成 Result<Request<B>, Response>
  4. Ok 继续 inner.call、Err 直接返 response

和 from_fn 对比:map_request 没有 Next 参数——用户不能自己决定什么时候调 inner,框架固定在用户函数返回后自动调。这让 map_request 拥有更窄的能力空间——换来代码更简单。

map_response:只加工 response

axum/src/middleware/map_response.rs:99-101:

pub fn map_response<F, T>(f: F) -> MapResponseLayer<F, (), T> {
    map_response_with_state((), f)
}

用法:

async fn set_header<B>(mut response: Response<B>) -> Response<B> {
    response.headers_mut().insert("x-foo", "foo".parse().unwrap());
    response
}

let app = Router::new()
    .route("/", get(handler))
    .layer(map_response(set_header));

和 map_request 对称——输入 Response<B>,输出 impl IntoResponse。中间件在 inner 处理完请求后运行、加工响应。

和 map_request 的关键差异

一、不支持 FromRequest(只支持 FromRequestParts):

// map_response 的 bound
$( $ty: FromRequestParts<S> + Send, )*  // 没有 $last: FromRequest

原因:map_response 的函数参数包括 response——request 已经被 inner 消费了。想从 request 提取信息(比如 Method、HeaderMap),只能用 FromRequestParts(&mut Parts 借用),不能用 FromRequest(消费 body)。body 已经被 inner 用了,消费不到。

这个约束是物理约束的类型化——和第 6 章讲过的 body 单次消费逻辑一脉相承。

二、不能短路:map_response 的用户函数返回 impl IntoResponse——没有 Err 分支。因为 inner 已经处理完了、response 已经在手里——"短路"没意义。想拒绝请求应该用 map_request 或 from_extractor,不要用 map_response。

三、依然能换 response 类型:函数返回 impl IntoResponse 不是 Response<B>——可以返回和原 response 完全不同的类型。比如根据原 response 的 status 生成一个完全新的 response:

async fn wrap_errors(response: Response) -> impl IntoResponse {
    if response.status().is_server_error() {
        // 把 5xx 包装成自定义错误格式
        (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "wrapped"}))).into_response()
    } else {
        response.into_response()
    }
}

Service impl 的核心

axum/src/middleware/map_response.rs:229-290 的核心 call:

// axum/src/middleware/map_response.rs:258-287 (简化)
fn call(&mut self, req: Request<B>) -> Self::Future {
    // ... clone inner, f, state ...
    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 req = Request::from_parts(parts, body);

        // inner 处理请求
        match ready_inner.call(req).await {
            Ok(res) => {
                // 用户函数加工 response
                f(T1, ..., res).await.into_response()
            }
            Err(err) => match err {}  // Infallible, 不可达
        }
    });

    ResponseFuture { inner: future }
}

关键不同点:inner.call(req) 在用户函数之前。这让 map_response 的用户函数拿到 response——不像 map_request 拿到的是 request。

这就让执行顺序固定为:"提取器 → inner.call → 用户函数"——用户无法改变。对于只需要 response 后处理的场景足够、对需要前后都处理的场景不够(得用 from_fn)。

map_response 的 Body 类型变化

一个微妙细节:map_response 函数的输入 body 类型 Response<B> 和输出 impl IntoResponse 里的 body 类型不必一致。比如:

async fn rewrap(response: Response) -> String {
    let (parts, body) = response.into_parts();
    // 把 body 缓冲成整块字节, 再换成一个 String 响应体
    let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap_or_default();
    format!("{}: {}", parts.status, String::from_utf8_lossy(&bytes))
}

输入是 inner 的 Response<ResBody>(挂在 Router 上时 inner 是 Route,ResBody 就是 axum 的 Body;axum::body::to_bytes 也只收 Body,所以这里不能写成泛型 Response<B>,否则编译报 E0308),输出只要 impl IntoResponse——map_response 不限制你必须返回同样的类型。

这种灵活性让 "响应加工" 能做更深的改造——比如压缩、加密、流式转非流式等。代价是每次都可能触发一次 body 再包装——对大 response 可能慢。

更详细的 map_response 实战

实际生产场景里 map_response 最常用来:给错误响应打上相关性 ID、按 handler 返回的 status 条件设 header、给特定响应加 body 包装。

一个综合示例:

async fn enrich_response(
    method: Method,                                   // Method 自身就是 FromRequestParts
    Extension(request_id): Extension<RequestId>,      // 更外层 middleware 塞的
    mut response: Response,
) -> Response {
    // 在响应 header 里附上 request_id, 客户端能看到
    response.headers_mut().insert(
        "x-request-id",
        HeaderValue::from_str(&request_id.0).unwrap(),
    );

    // 5xx 响应额外包裹错误元信息
    if response.status().is_server_error() && method != Method::HEAD {
        let status = response.status();
        let new_body = Json(json!({
            "error": "internal",
            "request_id": request_id.0,
            "status": status.as_u16(),
        })).into_response();
        return new_body;
    }

    response
}

let app = Router::new()
    .route("/api/data", get(data_handler))
    .layer(map_response(enrich_response))    // 内层
    .layer(map_request(inject_request_id));  // 外层:Router::layer 后写的更靠外,先看到请求

顺序在这里是必须的,不是风格问题:MapResponse::call 会先把自己的 FromRequestParts 提取器全跑完,然后才 inner.call(req)。所以 enrich_response 里的 Extension<RequestId> 必须在 inject_request_id 已经跑完之后才能取到值——map_request 必须在更外层。两行写反的话 Extension<RequestId> 会直接 rejection。

几个技巧:

  1. 复用外层 middleware 的 extensions:Extension<RequestId> 依赖更外层的 map_request 先跑
  2. 按 status 条件处理:5xx 完全替换 body、2xx/3xx 只加 header
  3. HEAD 方法别自己清 body:HEAD 响应不应该有 body,但 axum 顶层的 RouteFuture 会先按 body 的 size_hint 补 content-length、再把 body 清空(axum/src/routing/route.rs);在 map_response 里提前换成空 body,content-length 就成了 0(axum 0.8.9 实测:不清是 11,清了是 0)

更复杂的实战:from_extractor + 塞 extensions

from_extractor 的局限是丢弃提取值。想让提取值进入 handler,有两种模式:

模式一:handler 里再提取一次(推荐):

struct CurrentUser(User);
impl FromRequestParts for CurrentUser { /* 提取 */ }

// middleware 层做准入
.route_layer(from_extractor::<CurrentUser>());

// handler 里再提取(和 middleware 重复但必要)
async fn handler(CurrentUser(user): CurrentUser) { ... }

但第二次提取并不自动免费——FromRequestParts 没有任何内置缓存,CurrentUser 里如果有一次数据库查询,两次提取就是两次查询。想避免,要么在自己的提取器实现里做缓存(第一次成功后把值 insert 进 parts.extensions,后续先查 extensions),要么直接走模式二。除此之外,用户侧代码还有"重复"——签名里 middleware 和 handler 各写一次。

模式二:改用 from_fn 塞 extensions:

async fn auth(State(svc): State<AuthService>, headers: HeaderMap, mut req: Request, next: Next) -> Result<Response, StatusCode> {
    let user = svc.verify(&headers).await.map_err(|_| StatusCode::UNAUTHORIZED)?;
    req.extensions_mut().insert(user);
    Ok(next.run(req).await)
}

async fn handler(Extension(user): Extension<User>) { ... }

显式塞 extensions——handler 用 Extension 提取。代码更直接但 middleware 不再是 from_extractor。

实践中模式二更常见——因为大多数项目的 auth 逻辑不只是"提取检查",还会做 token refresh、usage tracking 等——from_fn 能做这些 side effect,from_extractor 纯检查不行。

执行时机:after 语义

map_response 是"after middleware"——它永远在 inner service 处理完后运行。这决定了它的用户函数感知不到 inner 的 poll_ready 状态:

  • 如果 inner 还没 ready,map_response 的 poll_ready 直接转发 inner 的——等 inner
  • inner ready 后 call 被调用——map_response 的函数还没参与
  • inner 处理 request 产出 response
  • map_response 的函数接收 response,执行加工

这个时序让 map_response 对 "请求阶段的事情" 完全不可见——你在这里没法拒绝请求、没法改 request、没法看 request body 是什么(除非提前用 FromRequestParts 拿了 headers / method)。这是它和 map_request 对称的设计:一个只看前、一个只看后。

需要既看前又看后的场景不适合 map_response——得用 from_fn。

from_extractor:用提取器做准入校验

axum/src/middleware/from_extractor.rs:89-91:

pub fn from_extractor<E>() -> FromExtractorLayer<E, ()> {
    from_extractor_with_state(())
}

用法:

use axum::{extract::FromRequestParts, middleware::from_extractor};

struct RequireAuth;

impl<S> FromRequestParts<S> for RequireAuth
where S: Send + Sync,
{
    type Rejection = StatusCode;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let auth_header = parts.headers.get("authorization").and_then(|v| v.to_str().ok());
        match auth_header {
            Some(s) if token_is_valid(s) => Ok(Self),
            _ => Err(StatusCode::UNAUTHORIZED),
        }
    }
}

let app = Router::new()
    .route("/", get(handler))
    .route("/foo", post(other_handler))
    .route_layer(from_extractor::<RequireAuth>());

核心:只写一个 FromRequestParts 实现、不需要另外写中间件函数——把 extractor 当中间件用。

Service impl:提取 + 丢弃 + 继续

axum/src/middleware/from_extractor.rs:198-232 的核心逻辑:

// axum/src/middleware/from_extractor.rs:215-231 (简化)
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()),
    }
}

逻辑极简:

  1. 拆 request 为 parts + body
  2. 跑 E::from_request_parts——这是整个中间件的业务逻辑
  3. 把 parts 和 body 重新组合回 request
  4. 进入 ResponseFuture 状态机等待 extract 结果,成功 → call inner、失败 → 直接返 rejection

提取值被丢弃:E::from_request_parts 返回的 Ok(extracted) 里的 extracted 值根本没用。这和 handler 里用 extractor 不同——handler 里提取值作为参数用,from_extractor 只用提取器的"成功/失败"信号。

ResponseFuture 的状态机

ResponseFuture 内部是一个状态机,状态定义在 axum/src/middleware/from_extractor.rs:248-260:

pin_project! {
    #[project = StateProj]
    enum State<B, T, E, S>
    where
        E: FromRequestParts<S>,
        T: Service<Request<B>>,
    {
        Extracting {
            future: BoxFuture<'static, (Request<B>, Result<E, E::Rejection>)>,
        },
        Call { #[pin] future: T::Future },
    }
}

两个状态:Extracting(等提取器完成)→ Call(等 inner service 返回)。这种 pin_project + 状态 enum 的写法是 Rust async Service 实现的典型模式——避免一次性 Box::pin 整个链条、让不同阶段的 future 独立推进。

相比之下,from_fn 和 map_request / map_response 都用一个 Box::pin 把"提取 → 调用户函数 → 调 inner"整条逻辑包成一个 BoxFuture。from_extractor 只把提取那一段装箱(extract_future),inner service 的 future 则由 State::Call { #[pin] future: T::Future } 原地持有——省掉了一层 box。

另一个容易被忽略的差别:FromExtractor 的 type Error = T::Error,不是 Infallible。它只是把 inner 的错误类型原样透传(inner 在 axum Router 里本来就是 Infallible,所以实际用起来看不出区别),rejection 走的是 Response 这条正常返回路径。

何时选哪种

四种 middleware 工具(加 from_fn)都各有适用场景:

需求 推荐选择
只改 header / 做前置校验(可短路)、不关心 response map_request
只在响应上加工(加 header、换格式、wrap errors)、不动 request map_response
准入校验(有 FromRequestParts 现成)、提取值不用进 handler from_extractor
既要前置又要后置处理、或逻辑复杂 from_fn
需要 poll_ready 或精细 Service 控制 原生 Tower Layer

对"认证"这个典型场景,三种都能做:

// map_request 版本
async fn auth<B>(req: Request<B>) -> Result<Request<B>, StatusCode> {
    verify(&req)?;
    Ok(req)
}
// ...layer(map_request(auth))

// from_extractor 版本
struct RequireAuth;
impl FromRequestParts for RequireAuth { /* ... */ }
// ...route_layer(from_extractor::<RequireAuth>())

// from_fn 版本
async fn auth(req: Request, next: Next) -> Result<Response, StatusCode> {
    verify(&req)?;
    Ok(next.run(req).await)
}
// ...route_layer(from_fn(auth))

选 from_extractor:如果认证逻辑是你已经写过的 FromRequestParts 提取器(handler 里本来也用它拿值),把 extractor 当中间件用避免重复。

选 map_request:如果认证只用 Result<Request, StatusCode> 短路、不需要塞 user 进 extensions 给 handler 用。

选 from_fn:如果认证后要在 response 上加点东西(比如 log 认证耗时)、或者 extractor 提取的值要 sideload 到 extensions 给 handler 用、或者代码涉及多步复杂逻辑。

大多数情况下 from_fn 是最通用的,哪种都不清楚就选它。三个专用 helper 存在是为了特定场景下代码更干净。

实战:用 map_response 做全局响应加工

典型场景:所有响应都加一组 security headers。用 map_response 可以在一个函数里同时加多个 header,并根据响应内容条件性地加:

use axum::{middleware::map_response, response::Response};

async fn security_headers(mut response: Response) -> Response {
    // 先读 status:下面的 h 可变借用 response 期间不能再调 response.status()(否则 E0502)
    let is_success = response.status().is_success();
    let h = response.headers_mut();
    h.insert("x-content-type-options", "nosniff".parse().unwrap());
    h.insert("x-frame-options", "DENY".parse().unwrap());
    h.insert("referrer-policy", "strict-origin-when-cross-origin".parse().unwrap());

    // 根据 status 条件性加 header
    if is_success {
        h.insert("cache-control", "public, max-age=60".parse().unwrap());
    } else {
        h.insert("cache-control", "no-store".parse().unwrap());
    }

    response
}

let app = Router::new()
    .route("/", get(handler))
    .layer(map_response(security_headers));

tower-http 的 SetResponseHeaderLayer 其实也能做条件性逻辑:它的 MakeHeaderValue<T> 对 FnMut(&T) -> Option<HeaderValue> 闭包有实现(tower-http-0.6.8/tower-http/src/set_header/mod.rs:30-37),闭包拿到 &Response、返回 None 就不设。但它一层只管一个 header,这里的四个 header 要叠四层;map_response 用一个普通 async 函数把它们写在一起。

from_extractor 的 body 消费限制

文档里一句关键警告(axum/src/middleware/from_extractor.rs:28-30):

Note that if the extractor consumes the request body, as String or Bytes does, an empty body will be left in its place. Thus won't be accessible to subsequent extractors or handlers.

这段文档注释已经过时,不能当行为依据:0.6 起 FromExtractor 的 Service 实现要求 E: FromRequestParts<S>(axum/src/middleware/from_extractor.rs:200),from_request_parts 只拿到 &mut Parts、碰不到 body。Bytes、String、Json<T> 这类消费 body 的提取器只实现了 FromRequest,根本挂不上去:

// ❌ 编译失败:E0277 the trait bound `Json<Payload>: FromRequestParts<()>` is not satisfied
.route_layer(from_extractor::<Json<Payload>>())

所以"body 被 from_extractor 吃空"在 0.8.9 里不会发生——约束由类型系统强制,是编译期错误而不是运行时坑(axum 0.8.9 实测)。from_extractor 只能搭配 FromRequestParts 提取器(Method、HeaderMap、Uri、自定义的 Parts 提取器);需要 body 校验的用 map_request 或 from_fn——它们能消费再重构 body 给 handler。

实战:三种 helper 组合使用

同一个 Router 里三种 helper 各司其职的例子:

use axum::{
    Router,
    routing::{get, post},
    middleware::{map_request, map_response, from_extractor},
};

let app = Router::new()
    .route("/api/data", get(data_handler))
    .route("/api/submit", post(submit_handler))
    // from_extractor: 认证(复用 handler 里也在用的 CurrentUser 提取器)
    .route_layer(from_extractor::<CurrentUser>())
    // map_request: 给 request 打上 tracing span id
    .layer(map_request(add_request_id))
    // map_response: 加全局 security headers
    .layer(map_response(security_headers));

async fn add_request_id<B>(mut req: Request<B>) -> Request<B> {
    let id = uuid::Uuid::new_v4().to_string();
    req.extensions_mut().insert(RequestId(id));
    req
}

async fn security_headers(mut res: Response) -> Response {
    let h = res.headers_mut();
    h.insert("x-content-type-options", "nosniff".parse().unwrap());
    h.insert("x-frame-options", "DENY".parse().unwrap());
    res
}

每个 middleware 的签名明确表达意图:

  • from_extractor::<CurrentUser>() → "必须带合法认证"
  • map_request(add_request_id) → "给请求加 ID"
  • map_response(security_headers) → "响应加安全头"

一眼看出三个中间件各自做什么、作用域。如果全部写成 from_fn,签名都是 fn(req, next) -> Response——读者要进函数体才知道谁做什么。专用 helper 让 middleware 声明即规范。

实战:用 map_request 做 Body 缓冲

API 签名验证需要读 body 做 HMAC check——从 body 算签名、对比 header 里的签名。这是 map_request 的典型场景:

use axum::{body::Bytes, middleware::map_request};

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

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

    if !verify_hmac(&bytes, signature) {
        return Err(StatusCode::UNAUTHORIZED);
    }

    // 重建 request 供 handler 使用
    Ok(Request::from_parts(parts, Body::from(bytes)))
}

注意 map_request 函数返回 Result<Request, StatusCode>——Ok 继续 inner、Err 短路 401。比 from_fn 写法少一行 next.run(req).await——语义等价但代码更短。

常见陷阱

坑一:map_response 用了 FromRequest 提取器:

// ❌ 编译失败 - map_response 只允许 FromRequestParts
async fn bad(Json(body): Json<Payload>, res: Response) -> Response { ... }

Json 是 FromRequest(消费 body)——map_response 拒绝。错误消息不会直接点名 Json,而是"MapResponse<fn(Json<Payload>, Response) -> ... {bad}, ...>: Service<Request<Body>> is not satisfied"(E0277,axum 0.8.9 实测),得自己对照 bound 找到是哪个参数不满足 FromRequestParts。修:在 handler 里处理、或改成 from_fn。

坑二:from_extractor 挂 body-consuming extractor:

// ❌ 编译失败:E0277 the trait bound `Bytes: FromRequestParts<()>` is not satisfied
.route_layer(from_extractor::<Bytes>())

这个坑编译器会拦下(见上一节,文档注释里"body 会被留空"的说法已过时)。修:换 FromRequestParts extractor。

坑三:map_request 里消费 body 但忘重构:

// ❌ 运行时 body 已消费, handler 拿不到
async fn bad<B>(req: Request<B>) -> Result<Request<B>, StatusCode> {
    let (parts, body) = req.into_parts();
    let _bytes = axum::body::to_bytes(body, 1024 * 1024).await.unwrap();
    // 忘了重新组装 req with bytes, 返回 parts + 已消费的 body
    Ok(Request::from_parts(parts, Body::empty()))  // body 是 empty 了
}

修:Body::from(bytes) 重新包装给 handler。

坑四:认证中间件没塞 user 进 extensions:

// ❌ from_extractor 丢弃 extracted value, handler 拿不到 user
struct CurrentUser(User);
impl FromRequestParts for CurrentUser { /* 提取 user */ }
.route_layer(from_extractor::<CurrentUser>())

async fn handler(Extension(user): Extension<User>) { /* user not found! */ }

from_extractor 丢弃提取值——handler 里 Extension<User> 找不到。修:要么 handler 里再用一次 CurrentUser 提取(重复提取)、要么改用 from_fn 主动塞 extensions。

坑五:map_response 做 body 检查(没 parts_only):

// ⚠️ 能跑但理解不直观 - map_response 之后 body 可能还是流式
async fn log_body(res: Response) -> Response {
    // res.body() 是 Body, 可能还没被消费
    // 如果想读 body 内容, 需要 to_bytes, 但 body 流式消费后无法再发
    res
}

map_response 看到的 response body 通常是流式——消费后无法 "退" 回原样给客户端。想真的读 response body 做 log,需要 buffer 后重构一个新 response——这会让流式响应失去流式性(SSE 场景会破)。复杂、小心用。

测试三个 helper

三种 helper 的测试方法类似——构造 Router 挂上 layer、发测试请求:

#[tokio::test]
async fn map_request_adds_header() {
    use tower::ServiceExt;

    let app = Router::new()
        .route("/", get(|headers: HeaderMap| async move {
            headers["x-added"].to_str().unwrap().to_string()
        }))
        .layer(map_request(|mut req: Request<Body>| async move {
            req.headers_mut().insert("x-added", "yes".parse().unwrap());
            req
        }));

    let res = app.oneshot(Request::new(Body::empty())).await.unwrap();
    let body = res.into_body();
    let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
    assert_eq!(&bytes[..], b"yes");
}

#[tokio::test]
async fn from_extractor_rejects_without_auth() {
    let app = Router::new()
        .route("/", get(|| async { "secret" }))
        .route_layer(from_extractor::<RequireAuth>());

    // oneshot 按值消费 Router,第一次要用 clone,否则下面再用 app 会报 E0382
    let res = app.clone().oneshot(Request::new(Body::empty())).await.unwrap();
    assert_eq!(res.status(), StatusCode::UNAUTHORIZED);

    let res_auth = app.oneshot(
        Request::builder()
            .header("authorization", "Bearer valid_token")
            .body(Body::empty())
            .unwrap()
    ).await.unwrap();
    assert_eq!(res_auth.status(), StatusCode::OK);
}

要点:

  • 单独测 middleware 效果:挂 layer 到一个最简单的 Router(handler 返回能验证效果的值)
  • 两路径都测:成功路径 + 失败路径(认证通过 / 不通过)
  • 不用真实 HTTP 客户端:oneshot 直接调 Router 的 Service——纯内存 tcp-less

三个 helper 源码结构的对比

把三个 helper 的核心 Service::call 放一起对比,设计差异一目了然:

// map_request 核心
fn call(&mut self, req) -> Self::Future {
    // 提取器 → f(extractors, req) -> Result<Request, E>
    // Ok(req) → inner.call(req)
    // Err(res) → 直接返 res
}

// map_response 核心
fn call(&mut self, req) -> Self::Future {
    // 提取器 → inner.call(req) -> res
    // f(extractors, res) -> impl IntoResponse
}

// from_extractor 核心
fn call(&mut self, req) -> Self::Future {
    // E::from_request_parts(parts, state)
    // Ok(_) → inner.call(req)
    // Err(rejection) → rejection.into_response()
}

// from_fn 核心(对比)
fn call(&mut self, req) -> Self::Future {
    // 提取器 → f(extractors, Next{inner}) -> impl IntoResponse
    // 用户在 f 里调 next.run(req)
}

观察:三个 helper 把 inner.call 的调用位置固定在框架内——用户函数不接触 inner、由框架决定"何时 call"。这是和 from_fn 的根本区别——from_fn 把 next.run 交给用户。

这种"收起 next 的控制权"是专用 helper 的关键设计:

  • map_request 固定"用户函数之后 call inner"
  • map_response 固定"inner call 之后调用户函数"
  • from_extractor 固定"提取成功后调 inner"

用户只写"做什么"、不决定"何时做"——API 更简单、不会弄错。

Helper 和 tower-http 的互补

三个 helper 和 tower-http 的几个中间件在功能上部分重叠:

场景 helper 方案 tower-http 方案
给响应加固定 header map_response SetResponseHeaderLayer(tower-http-0.6.8/tower-http/src/set_header/response.rs:112)
给请求加 header map_request SetRequestHeaderLayer(tower-http-0.6.8/tower-http/src/set_header/request.rs:99)
按 header 做认证 from_extractor<Auth> ValidateRequestHeaderLayer(tower-http-0.6.8/tower-http/src/validate_request.rs:137)
条件性响应加工 map_response + 自定义逻辑 单个 header 可用 SetResponseHeaderLayer + 闭包(FnMut(&Response) -> Option<HeaderValue>),更复杂的加工不直接支持
body 缓冲/校验 map_request + to_bytes RequestBodyLimitLayer(tower-http-0.6.8/tower-http/src/limit/layer.rs:12)等

选择建议:

  • tower-http 方案成熟:长期维护、有测试、零配置就能用。优先用
  • axum helper 更灵活:需要 axum 特定能力(state、提取器)或自定义逻辑时用

混用是最优解——tower-http 做标准能力、helper 做业务特定。学习成本上 tower-http 需要读它的 API 文档、helper 只要你会 axum 基础就行。

pin_project 与 BoxFuture 的选择

细读源码会发现:

  • map_request / map_response / from_fn 都用 BoxFuture(Box::pin 包住 async block)
  • from_extractor 用 pin_project! 状态机

差异:

BoxFuture:

  • 代码简单(直接 async move {...})
  • 运行时每请求一次堆分配
  • 内部逻辑对 pin-projection 的 footprint 隐藏

pin_project 状态机:

  • 代码复杂(enum 多状态 + poll 方法手写)
  • 只有必须装箱的那一段(from_request_parts 的 future)进 Box,inner 的 future 用 #[pin] 原地持有
  • 分配更少但复杂度高

注意别把 pin_project 等同于"零分配"——FromExtractor::call 里那句 let extract_future = Box::pin(async move { ... });(axum/src/middleware/from_extractor.rs:219)依然是一次堆分配,省下的是 inner future 的那一层。至于为什么三个 helper 实现风格不统一,源码里没有留下说明,从版本时间线上看更像是逐步添加时的取舍:from_extractor(0.5.4)先落地了手写状态机,后来的 map_request / map_response(0.6.0)沿用了 from_fn 的 BoxFuture 写法。

对用户侧完全透明——选什么 helper 不影响你的代码。

map_request 的 body 类型约束

看 map_request 的 bound(axum/src/middleware/map_request.rs:265-266):

B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,

从签名看这比 from_fn 宽松——FromFn 只实现了 Service<Request>(也就是 Request<Body>),MapRequest 则对任意满足 HttpBody<Data = Bytes> 的 B 都写了 Service<Request<B>> 的 impl。call 的第一行 let req = req.map(Body::new); 立刻把泛型 body 换成 axum 的 Body,所以用户函数拿到的仍然是统一后的类型。

但这份宽松在实际写法里用不上。同一组 bound 里还有 Fut::Output: IntoMapRequestResult<B>(map_request.rs:258)和 I: Service<Request<B>>(:259):用户函数返回的 request 会原样交给 inner,它的 body 类型必须就是外面进来的那个 B。而用户函数的最后一个参数是 FromRequest,Request 只以 Request<Body> 的形式实现它,返回的自然也是 Request<Body>,于是 B 被钉成 Body。实测把 map_request(add_header) 套在一个 Service<Request<String>> 上,oneshot 直接报 E0599(trait bound 不满足,axum 0.8.9)。

所以别指望用 map_request 去包"body 还没统一成 axum::body::Body"的服务——除非用户函数自己把 Request<Body> 转回那个 body 类型。挂在 Router 上时 body 本来就是 Body,这个差异看不出来。

三个 helper 的边界情况

case:from_extractor 加了个不 Send 的 state

// 构造函数本身不设约束;挂上去时 Service impl 要求 state: Clone + Send + Sync + 'static
from_extractor_with_state::<MyExtractor, _>(my_state)

如果 my_state 的某个字段不 Send(比如嵌套了 Rc),会编译失败。修:用 Arc<T> 替换 Rc、或者 restructure state。

case:map_request 的函数参数顺序

// ✅ 对的: Request 在最后
async fn good(method: Method, req: Request) -> Request { ... }

// ❌ 错的: Request 必须是最后一个
async fn bad(req: Request, method: Method) -> Request { ... }

和 handler / from_fn 一样——遵守"前面 FromRequestParts、最后 FromRequest"的规则。

case:map_response 的响应类型变换

map_response 函数返回 impl IntoResponse 不是 Response——能返回和输入类型完全不同的响应:

async fn transform<B>(response: Response<B>) -> String {
    format!("wrapped: {}", response.status())
}

这让 map_response 能做"彻底重写响应"。代价:丢失了 inner response 的所有信息(status、headers、body)——通常想做加工而不是重写时小心别这么写。

共用设计模式

三个 helper 都共享几个模式——理解后读任何一个源码都容易:

一、_with_state 带 state、不带 state 的是 ((), f):同第 13 章讲的 from_fn(axum/src/middleware/from_fn.rs:114)/ from_fn_with_state(axum/src/middleware/from_fn.rs:164),from_extractor(axum/src/middleware/from_extractor.rs:89)/ from_extractor_with_state(axum/src/middleware/from_extractor.rs:96)也是同一对形状。所有 axum 中间件 helper 都这样提供两版 API。

二、mem::replace + clone 拿走 ready Service:第 12、13 章都讨论过的 Tower 惯用模式。map_request 和 map_response 的 call 方法都有这段(from_extractor 没有:它直接 self.inner.clone() 放进 ResponseFuture,见 from_extractor.rs:229)——let mut ready_inner = std::mem::replace(&mut self.inner, not_ready_inner);(axum/src/middleware/map_request.rs:281、axum/src/middleware/map_response.rs:260)。

三、用宏展开出多份 impl:map_request 用 all_the_tuples!(impl_service)(宏体 axum/src/middleware/map_request.rs:247,展开点 320),展开 1-16 个参数(最后一个必须是 FromRequest);map_response 自己列了 impl_service!() 到 impl_service!(T1, ..., T16) 共 17 份(axum/src/middleware/map_response.rs:292-308),包含零提取器版本——因为它的"最后一个参数"是 Response<ResBody> 而不是提取器。from_extractor 根本不需要宏,它只有一个类型参数 E。

四、函数体内用 BoxFuture 或 pin_project 状态机:BoxFuture 最简单(from_fn / map_request / map_response 都用,见 axum/src/middleware/map_request.rs:287、axum/src/middleware/map_response.rs:266 的 Box::pin(async move { ... })),pin_project 状态机更精细(from_extractor 用,axum/src/middleware/from_extractor.rs:7、234、248)。两者的性能差别只在多不多一次堆分配,根据实现优雅度选。

五、短路路径一律转成 Response:三者的拒绝分支都是 rejection.into_response() / Err(res) => res,绝不把错误抬成 Service::Error。MapRequest 和 MapResponse 的 type Error = Infallible 写死(axum/src/middleware/map_request.rs:270、axum/src/middleware/map_response.rs:250);FromExtractor 稍有不同——type Error = T::Error(axum/src/middleware/from_extractor.rs:207),原样透传 inner 的错误类型(在 axum Router 里 inner 本就是 Infallible)。

这些共享模式说明 axum 的中间件系统不是每个 helper 独立设计的——而是一套 pattern 的不同变体。读懂一个就理解其他。

设计 pattern 延伸到自定义 middleware

如果你要写一个比这四种 helper 都不合适的 middleware(特殊场景),按上述模式写原生 Tower Layer/Service 会让代码有"axum 风格":

  1. 写 YourLayer 和 YourService 两个类型
  2. YourService 的 Service impl 里用 clone-and-replace 保持 Tower readiness 语义
  3. Error 收敛到 Infallible(把 Err 转 Response)
  4. 用 BoxFuture 或 pin_project 取决于你的性能需求
  5. 如果想支持 FromRequestParts 提取器,用宏展开 1-16 个

这种 axum 风格的自定义 Layer/Service 和原生 tower-http 里的 middleware(比如 CompressionLayer)结构相似——因为它们都遵守同一套模式。读懂 axum 的四个 helper 源码,你就学会了写 axum 生态所有 middleware 的骨架。

middleware 之间的数据共享

四种 helper 之间、middleware 和 handler 之间共享数据有几个途径:

  • extensions:最通用、按类型(TypeId)索引的动态 map,每种类型存一个值。所有 middleware 和 handler 都能读写
  • 函数参数 (extractor):类型级明确、编译期检查。只能读 extensions 的现有数据
  • 状态 (State):Router 级一致、FromRef 派生子 state
  • tracing span fields:通过 #[instrument] 或 info_span! 的 fields——这是只读的日志上下文

工程经验:

  • 请求级动态数据(request id、user context、trace span)→ extensions
  • Router 级静态数据(数据库连接池、配置、客户端)→ State
  • 中间件提取的只读信息供 handler 用(验证的 user、parsed token)→ 塞 extensions 再 Extension<T> 读

这几种通道组合起来能覆盖所有 middleware ↔ handler 的数据交换需求——比通过 "传字段" 耦合各层更干净。

实战第四种:条件性 cache header

一个生产常见场景——根据响应 status 决定 Cache-Control:

// 参数写 Response(即 Response<Body>)而不是泛型 Response<B>:
// 元组里的 response 要实现 IntoResponse,泛型 B 缺 HttpBody 等约束会报 E0277
async fn cache_header(response: Response) -> impl IntoResponse {
    let value = match response.status().as_u16() {
        200 | 203 | 206 => "public, max-age=300",
        301 | 302 => "public, max-age=60",
        404 => "public, max-age=5",          // 短缓存让不存在的资源也被 CDN 缓一下
        401 | 403 => "no-store",             // 与身份相关,不进公共缓存
        500..=599 => "no-store",
        _ => "public, max-age=10",
    };
    ([("cache-control", value)], response)
}

let app = Router::new()
    // axum 0.8 起通配符路由写作 `{*rest}`,旧的 `/*path` 语法已经不再接受
    .route("/{*rest}", get(handler))
    .layer(map_response(cache_header));

特点:

  1. 按 status 精细化 cache-control:2xx 长缓存、3xx 中缓存、4xx 短缓存、5xx 不缓存
  2. 元组响应 + response:返回 ([("cache-control", ...)], response) 元组——内置 IntoResponse impl 自动合并
  3. 对比 tower-http:SetResponseHeaderLayer::overriding(CACHE_CONTROL, |res: &Response| ...) 用闭包也能按 status 返回不同的值(返回 None 则不设);map_response 的优势是写法就是普通 async 函数,还能用提取器和 state

tower-http 覆盖默认行为好、axum helper 覆盖定制行为好——两者互补。

IntoResponseFailed 与 map_response 的相互作用

版本口径:IntoResponseFailed 只存在于 axum 主线(本专栏基准 commit de9f13d),不在 crates.io 的 axum 0.8.9 / axum-core 0.5.6 里,详见第 9 章。

有一个跨章的细节值得在这里点一下:map_response 的用户函数返回 impl IntoResponse,而元组形式的 IntoResponse(比如 ([("cache-control", v)], response))在遇到带 IntoResponseFailed 扩展标记的响应时,不会用元组里的状态码去覆盖它(axum-core/src/response/into_response.rs 里那几处 res.extensions().get::<IntoResponseFailed>().is_none() 判断)。

也就是说:如果 handler 在序列化 Json<T> 时失败、响应已经被打成 500 并带上了 IntoResponseFailed,你在 map_response 里写 (StatusCode::OK, response) 也改不回 200。这是第 9 章讲过的机制在中间件层的自然延续——错误响应的状态码不会被外层装饰意外改掉。

middleware 和错误处理的集成

中间件的错误处理和第 12 章讲的错误模型无缝衔接:

  • middleware 短路 Err:Result<T, E: IntoResponse> 的 Err 自动转 Response——和 handler 一致
  • 中间件 panic:外层 CatchPanicLayer 捕获——所有 middleware 类型都一样
  • 内部中间件返回 Err:helper 内部把 Err 转 Response、不会暴露给 Service::Error(map_request / map_response 写死 Infallible;from_extractor 透传 inner 的 Error,挂在 Router 里同样是 Infallible)

这让你写 middleware 时不用重新学错误规则——和 handler 里的 ? + Result<T, E> 完全一样。middleware 签名里 Result<Request, StatusCode> 或 Result<Response, AppError> 的 Err 类型只要 IntoResponse 就行。

一个坑是:middleware 短路产生的 response 和 handler 正常产生的 response 在下游看来没区别——都是普通 Response。所以下游的 map_response 会加工两者——如果你的安全 header middleware 在 auth middleware 外层,auth 401 响应也会加上那些 header。大多数场景这是 OK 的(401 响应也该带 security header)——但偶尔不想让某个 header 出现在错误响应里时,在 middleware 里手动判断 status。

常见生产 middleware 清单

axum 生态(axum-extra + tower-http + 自己写)里生产项目常见的 middleware:

Middleware 实现 用途
Tracing / Logging tower-http TraceLayer 请求日志 + tracing span
CORS tower-http CorsLayer 跨域控制
Compression tower-http CompressionLayer gzip / brotli 响应压缩
Timeout tower-http TimeoutLayer::with_status_code(直接返给定状态码,如 408;TimeoutLayer::new 自 0.6.7 起已弃用),或 tower TimeoutLayer + HandleErrorLayer 请求超时
Request body limit tower-http RequestBodyLimitLayer body 大小限制
Concurrency limit tower GlobalConcurrencyLimitLayer(Error = S::Error,不用配 HandleErrorLayer;别用 ConcurrencyLimitLayer,Router::layer 给每个端点各套一份、信号量各自独立,见第 12 章) 并发数控制
CatchPanic tower-http CatchPanicLayer panic 捕获
Auth 自定义 from_extractor 或 from_fn 认证
Rate limit tower_governor 或自定义 from_fn 限流
Request ID tower-http SetRequestIdLayer + PropagateRequestIdLayer,或自定义 from_fn 生成并传播请求 ID
Security headers map_response 加安全头
Cache-Control map_response 按 status 定缓存策略

前面七个是 Tower / tower-http 开箱即用的;后面五个通常自己写(Request ID 也有 tower-http 现成实现)。典型 axum 应用的 middleware 栈是几层 tower-http 加几层自定义,每层职责清晰。

测试 middleware 栈的综合模式

完整测试中间件栈需要能控制端到端行为。最佳实践:

mod test_helpers {
    use axum::{routing::{get, Route}, Router};
    use tower::{Layer, Service};

    // bound 抄自 `Router::layer` 的签名(axum/src/routing/mod.rs)
    pub fn test_app<L>(extra_layer: L) -> Router
    where
        L: Layer<Route> + Clone + Send + Sync + 'static,
        L::Service: Service<axum::extract::Request> + Clone + Send + Sync + 'static,
        <L::Service as Service<axum::extract::Request>>::Response:
            axum::response::IntoResponse + 'static,
        <L::Service as Service<axum::extract::Request>>::Error:
            Into<std::convert::Infallible> + 'static,
        <L::Service as Service<axum::extract::Request>>::Future: Send + 'static,
    {
        // 测试专用 Router - 只有一个简单 handler
        Router::new()
            .route("/test", get(|| async { "ok" }))
            .layer(extra_layer)
    }
}

#[tokio::test]
async fn security_headers_are_added() {
    use tower::ServiceExt;
    let app = test_helpers::test_app(map_response(security_headers));

    let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
    let res = app.oneshot(req).await.unwrap();
    assert_eq!(res.headers().get("x-frame-options").unwrap(), "DENY");
}

#[tokio::test]
async fn auth_middleware_rejects_without_token() {
    let app = Router::new()
        .route("/test", get(|| async { "secret" }))
        .route_layer(from_extractor::<RequireAuth>());

    // 必须请求 /test:Request::new 的 URI 是 "/",走 fallback 得 404,route_layer 不管 fallback
    let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
    let res = app.oneshot(req).await.unwrap();
    assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn auth_middleware_passes_with_valid_token() {
    // ... 类似上面但带 valid header
}

要点:

  • test_app 工厂函数:复用配置给不同测试
  • oneshot:直接测 Service 层不起真实 TCP
  • 多个正负测试:每个 middleware 至少一个成功 + 一个失败路径
  • middleware 自己 unit test + Router 级 integration test 混用——unit test 快、integration test 全链路

中间件逻辑越复杂越应该写 unit test——拆开每条逻辑单独验证、比每次跑 Router 快得多。

对比:handler 本身能不能当中间件

细心的读者会发现:map_request 的函数签名 (extractor..., Request) -> Result<Request, E> 和 handler 签名 (extractor..., Request) -> impl IntoResponse 非常像。handler 能当中间件用吗?

不能——handler 返回 Response(处理完成),map_request 返回 Request(继续处理)。语义不同:

  • handler:是处理链的终点。参数都提取完就该业务执行、产出响应
  • map_request:是处理链的中间。函数执行后必须有东西能继续给下一层

框架用不同的 trait 表达这个区分:handler 的 return 是 IntoResponse、map_request 的 return 是 IntoMapRequestResult。后者更窄——只允许 Request 或 Result<Request, E>。

这种设计让 axum 里每种中间件的语义在 API 层显式化——你看函数签名就知道这个中间件是"终止"、"透传"、还是"只加工一边"。

handler-like vs layer-like 两种思维

axum 的 middleware API 分为两种心智模型:

handler-like(from_fn / map_request / map_response / from_extractor):

  • 签名像 handler(async fn + 提取器)
  • 不需要手写 Service trait
  • 不需要理解 poll_ready / call 的 Tower 契约
  • 适合"业务逻辑视角"的中间件

layer-like(原生 Tower Layer + Service):

  • 定义 Layer 和 Service 两个类型
  • 手写 Service trait impl
  • 需要理解 poll_ready / call / Future 三件套
  • 适合"HTTP 基础设施视角"的中间件

对初学者:handler-like 入门容易——和写 handler 一样的心智模型。对 Tower 熟手:layer-like 更直接——完全控制每一环节。

axum 的高明之处是同时支持两种——没逼用户必须学 Tower 才能写中间件(Rocket / actix-web 都是自建 trait 体系,离 Tower 远)、也没把中间件完全收进一套 handler 式的自有抽象、把 Tower 生态隔在外面。axum 的混合策略让它既能独立开发体验好、又能和 Tower 生态无缝协作。

这种"两种思维并存"也是 axum 中间件一章涵盖内容多的原因——不只一个工具要讲、不只一个心智模型要建立。但一旦两边都学会,axum 的中间件系统就像"瑞士军刀"——小工具做小事、大工具做大事、每种都合适自己的场景。

性能剖析

四种中间件工具的每请求开销差异:

工具 每请求堆分配 dyn Service 调用
原生 Tower Layer 0(如果自己写具名 Future) 0
from_extractor 1 × Box::pin(只包提取那一段) 0
map_request 1 × Box::pin(包整条逻辑) 0
map_response 1 × Box::pin(包整条逻辑) 0
from_fn 1 × Box::pin + 1 × BoxCloneSyncService::new + 调 next.run 时 inner future 再 Box::pin 一次 1(Next::inner.call)

(各 helper 用 Box::pin 装出的 BoxFuture 本身是 Pin<Box<dyn Future>>,poll 时同样经 vtable;最后一列只数经过类型擦除的 Service 调用。from_fn 第三次分配来自 BoxCloneSyncService::new 里的 map_future(|f| Box::pin(f)),tower-0.5.3/tower/src/util/boxed_clone_sync.rs:32。)

排序很直观:原生 Layer 最省,from_fn 最贵(多两次装箱 + 一次虚调用)。

这些差异在真实项目里基本看不出来——handler 的业务逻辑通常涉及数据库或网络 IO,量级远大于几次堆分配。选择主要看代码可读性和开发效率,不是性能。

FAQ:工程决策

Q:项目里所有 middleware 都用 from_fn,会有问题吗?

没问题。功能上完全一致。代价是 1) 每个签名都带 Next、读者不能立刻分辨 middleware 类型 2) 每请求多出 BoxCloneSyncService 带来的两次装箱和一次虚调用。对多数项目这个代价可以忽略,团队统一用 from_fn 也是合理选择。

Q:map_request 和 from_extractor 功能重叠怎么选?

  • map_request 返 Request 或 Result<Request, E>——函数是通用 async fn
  • from_extractor 必须有一个 FromRequestParts 类型——"extractor 本身就是 middleware 逻辑"

选择标准:

  • 如果你已经有 FromRequestParts 实现(在 handler 里也在用) → from_extractor
  • 如果逻辑是"一次性"的 async 函数、不想单独写个类型 → map_request

Q:from_extractor 能用 state 吗?

能。from_extractor_with_state::<E, _>(state)——这时 E 的 FromRequestParts<S> bound 里 S 是 state 类型。

Q:middleware 里可以用 tokio::spawn 做 fire-and-forget 吗?

可以,all helper 都支持——spawn 的 task 独立运行不阻塞响应。但 spawn 的 task 发生在 Router 请求处理之外——panic 不会被 CatchPanicLayer 抓到。要自己 log 或加 catch_unwind。

Q:多个 middleware 顺序重要吗?

重要。顺序决定洋葱的层次。一般的原则:认证靠外(尽早拒绝非法请求)、rate limit 在认证之后(需要 user identity)、压缩靠外(压缩层外面的 middleware 看到的都是压缩后的 body,要读、要改响应 body 的 middleware 应放在压缩层里面)。

从 from_fn 迁移到专用 helper 的重构示例

看一个具体重构。原始 from_fn 代码:

async fn add_cors(request: Request, next: Next) -> Response {
    let mut response = next.run(request).await;
    response.headers_mut().insert("access-control-allow-origin", "*".parse().unwrap());
    response
}

这个函数明显只加工 response——用 map_response 更直接:

async fn add_cors(mut response: Response) -> Response {
    response.headers_mut().insert("access-control-allow-origin", "*".parse().unwrap());
    response
}

少一层嵌套、签名直接说明意图。类似地,"只改请求"的 from_fn 应该重构成 map_request、"只做准入"的重构成 from_extractor。

这种按场景迁移让项目整体可读性提高——阅读的人看签名就知道 middleware 类别,不用深入函数体猜意图。

想自己量一下开销?

如果你确实关心这几个 helper 的相对开销,别信任何二手数字——本机跑一遍才作数。做法是:写四个功能完全相同的中间件(比如都只给 request 加一个 header),分别用原生 Layer、map_request、from_extractor、from_fn 实现,挂到同一个只返回常量字符串的 Router 上,用 criterion 直接 bench Router 的 oneshot(绕开 TCP 和 hyper,只测中间件栈本身)。

可以预期的是:差异集中在上一节那张表里的"每请求多几次堆分配、多一次虚调用"上,而一旦 handler 里出现任何一次数据库或网络往返,这点差异就被完全淹没。这也是 axum 敢提供这么多 helper 的底气——它们之间的性能差距小到不该成为选型依据,挑 API 最贴合场景的那个就行。

跨专栏关联:四种中间件的设计哲学

把第 13、14 章讲的四个中间件工具(map_request、map_response、from_extractor、from_fn)放在 Tower / axum 架构图里:

flowchart TB
    subgraph "能力递增"
        direction LR
        FE["from_extractor<br/>只准入校验"] -->
        MR["map_request<br/>+ 改请求"] -->
        MRE["map_response<br/>+ 改响应"] -->
        FF["from_fn<br/>+ 前后钩子"] -->
        LAYER["原生 Tower Layer<br/>+ poll_ready / 状态管理"]
    end
    subgraph "代码简洁度递减"
        direction RL
        FE2[最简] --> MR2 --> MRE2 --> FF2 --> LAYER2[最繁琐]
    end

设计原则是:每个 helper 都在"简洁"和"能力"之间做权衡。能力窄的更简洁、能力广的更通用。用户按需选择——axum 不强制你用最通用的(from_fn),也不逼你用最底层的(Layer)。

这种"分层 API"设计和 Tokio 的 Future / tokio::select! / tokio::spawn 分层类似——不同用户对不同抽象层级有需求、都要满足。

《Hyper 与 Tower:工业级 HTTP 栈》专栏第 24 章(§24.3 "Layer 的类型级设计"、§24.8 "可迁移的设计模式")讨论过这种"面向使用场景的 API 分层"思想。axum 的中间件系统是那套思想在 HTTP 框架层的具体应用——根据 middleware 常见用法(准入校验、请求加工、响应加工、全能处理)提供专门的 API 简化各自场景。

中间件栈的可视化

把第 13、14 章的所有中间件工具放一起画:

flowchart TD
    REQ[Request] --> ROUTER[Router]
    ROUTER --> OUTER{"最外层 Layer"}
    OUTER --> L1["map_response<br/>security_headers<br/>(全局响应加工)"]
    L1 --> L2["TraceLayer<br/>(Tower)"]
    L2 --> L3["from_fn<br/>log_request<br/>(请求日志)"]
    L3 --> L4["map_request<br/>inject_request_id<br/>(请求注入)"]
    L4 --> L5["from_extractor<br/>CurrentUser<br/>(认证校验)"]
    L5 --> L6["from_fn<br/>rate_limit<br/>(按用户限流)"]
    L6 --> CORE["HandlerService"]
    CORE -.响应.-> L6 -.-> L5 -.-> L4 -.-> L3 -.-> L2 -.-> L1 --> RES[Response]
    style L1 fill:#fef
    style L3 fill:#ffe
    style L4 fill:#efe
    style L5 fill:#eef
    style L6 fill:#ffe

每种颜色区分 middleware 种类:

  • 粉:map_response - 仅加工响应
  • 黄:from_fn - 通用中间件
  • 绿:map_request - 仅改请求
  • 蓝:from_extractor - 仅校验

签名就能看出职责——读 Router 代码像读系统架构图。这是 axum 中间件体系的美学。

历史演进

这四个 middleware helper 在 axum 的出现时间:

  • from_fn(axum 0.4.6,2022-02-22):最早落地,解决"不想手写 Layer"的问题
  • from_extractor(axum 0.5.4,2022-04-26):发现很多 middleware 就是"跑一次 extractor"——把它提取成独立 helper,同时弃用了更早的 extract::extractor_middleware(#957)
  • map_request / map_response(axum 0.6.0,2022-11-25):观察到大量 middleware 只做单向变换——再专门化;同一个版本里 from_fn 也拿到了提取器支持和 from_fn_with_state

每一版都基于对使用模式的观察做提炼:用户实际写的是什么样的中间件?能不能针对那种写法做 API 简化? 从 0.4.6 到 0.6.0 的演进就是这套观察的结果。

0.6 之后这几个 helper 的 API 面基本稳定,新增的同类工具主要落在 axum-extra(比如 axum_extra::middleware::option_layer,把 Option<Layer> 变成可以直接 .layer() 的东西),axum 本体保持简洁。

小结

第 13、14 两章讲了 axum 的中间件体系。核心信条:

  • Tower Layer 是底层基础:通用、能力全、代码繁琐
  • from_fn 是常用封装:能力接近 handler、适合复杂业务中间件
  • map_request / map_response / from_extractor 是专用 helper:能力窄但语义更清晰

项目里混用是常态——关键是让每个中间件的语义和选择一致:准入校验用 from_extractor、改请求用 map_request、加工响应用 map_response、复杂逻辑用 from_fn。读代码的人看到工具选择就能猜到中间件意图——比所有都写成 from_fn 更有信息量。

对中间件系统的整体回顾

到这里 axum 的中间件机制已经完整覆盖:

  • Tower Layer + Service(第 12 章讲错误处理时涉及):最底层、能力最全
  • from_fn / from_fn_with_state(第 13 章):函数式全能
  • map_request / map_response / from_extractor(本章):按场景专门化

这三个层级覆盖所有中间件需求。学习曲线上:

  • 新手:先用 from_fn——最灵活、签名接近 handler、容易理解
  • 进阶:看到代码里只改 request / response / 只校验,换成 map_* 或 from_extractor——意图更明显
  • 专家:性能关键路径改写原生 Tower Layer——省下 BoxFuture 开销

工程品味上:

  • 项目初期 middleware 全用 from_fn,代码一致容易读
  • 代码熟了,有 1-2 个中间件特别适合 map_* / from_extractor,重构改写——让读的人一眼看出意图
  • 生产化后如果某 middleware 在 profile 里显眼,考虑改成原生 Layer

这种"从粗到细"的演进路径——一开始不用过早优化、代码稳定后再按需 specialize。

middleware 选择 rubric

给出一个实战 rubric 帮助中间件选型:

条件 选择
只做 request 校验、不需要中间件函数体 from_extractor(复用现成 extractor)
只改 request / 准入校验、逻辑简短 map_request
只在响应上加工、不干预请求 map_response
需要前后都处理或复杂逻辑 from_fn
需要 poll_ready、复杂 state、发布给其他 tower 项目 原生 Tower Layer

此 rubric 覆盖绝大多数场景。剩下的边界情况(比如"需要同时访问 state 和 inner service"、"需要精细 error mapping")是 tower::Service 的领域——本专栏第 12 章讨论 HandleError 时已部分涉及。

下一章进入 axum 的运行层——serve 如何监听端口、接受连接、调度 handler。前面章节都是在 axum 的抽象层里讨论,第 15 章开始我们下沉到 tokio / hyper 之上、axum 怎么把 Router(一个 Service)粘到 TCP 监听器上。那一层讨论完整的运行时生命周期——包括优雅关闭。从 14 章的函数式中间件到 15 章的连接层,视角从"请求是一次调用"下降到"请求是一个网络事件"——要转换思维。