Axum 设计与实现
第14章 map_request / map_response / from_extractor:变换式中间件
第 13 章讲的 from_fn 是能力最广的中间件形式——前后钩子、短路、提取器、state——什么都能做。但很多场景只需要更受限的能力:只改 request、只加工 response、只做准入校验。axum 为这些常见场景提供了三个更"专用"的 helper:
map_request:只修改 request(或短路拒绝),不接触 responsemap_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)
}
}
两种返回类型都合法:
Request<B>:直接返回修改后的 request——总是继续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,
}
});
逻辑很清晰:
- 依次调用提取器(和 handler / from_fn 一致)
- 把提取值传给用户函数
f - 用户函数返回值经
into_map_request_result统一成Result<Request<B>, Response> - 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。
几个技巧:
- 复用外层 middleware 的 extensions:
Extension<RequestId>依赖更外层的 map_request 先跑 - 按 status 条件处理:5xx 完全替换 body、2xx/3xx 只加 header
- 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()),
}
}
逻辑极简:
- 拆 request 为 parts + body
- 跑
E::from_request_parts——这是整个中间件的业务逻辑 - 把 parts 和 body 重新组合回 request
- 进入 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
StringorBytesdoes, 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 风格":
- 写
YourLayer和YourService两个类型 YourService的 Service impl 里用 clone-and-replace 保持 Tower readiness 语义- Error 收敛到 Infallible(把 Err 转 Response)
- 用 BoxFuture 或 pin_project 取决于你的性能需求
- 如果想支持 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));
特点:
- 按 status 精细化 cache-control:2xx 长缓存、3xx 中缓存、4xx 短缓存、5xx 不缓存
- 元组响应 + response:返回
([("cache-control", ...)], response)元组——内置 IntoResponse impl 自动合并 - 对比 tower-http:
SetResponseHeaderLayer::overriding(CACHE_CONTROL, |res: &Response| ...)用闭包也能按 status 返回不同的值(返回None则不设);map_response 的优势是写法就是普通 async 函数,还能用提取器和 state
tower-http 覆盖默认行为好、axum helper 覆盖定制行为好——两者互补。
IntoResponseFailed 与 map_response 的相互作用
版本口径:
IntoResponseFailed只存在于 axum 主线(本专栏基准 commitde9f13d),不在 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 章的连接层,视角从"请求是一次调用"下降到"请求是一个网络事件"——要转换思维。