Appearance
第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 是"for the common case"的设计——axum 团队观察到大多数实际中间件只做某一类工作(仅改 request、仅改 response、仅做校验),没必要都用全能的 from_fn。提供专用 API 让常见情况更简洁、特殊情况回退到 from_fn 或 Tower Layer。这种"常用场景优先、特殊场景兜底"的 API 设计风格在 axum 各处都见——提取器有 OptionalFromRequest、响应有 NoContent、中间件有本章讨论的三兄弟。
三者的职责对比
先放一张总览图说明三者在请求处理流程里的位置:
三者都比 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 即可。那为什么还要这三个?
答案是读代码的人受益。
考虑两段逻辑等价的代码:
rust
// 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、少一次 Result 嵌套——读的人看函数签名就知道"这只改 request、不看 response"。from_fn 版本虽然简单,但签名里带着 Next——读者要读完函数体才能确定作者有没有做 response 后处理。
这层"意图可读性"是专用 helper 存在的真实理由——不是性能、不是功能、而是让读代码的人负担更小。一个项目里每种中间件类型都用合适的 helper,签名即文档——维护时减少"猜作者意图"的时间。
map_request:只改 request
axum/src/middleware/map_request.rs:117-119:
rust
pub fn map_request<F, T>(f: F) -> MapRequestLayer<F, (), T> {
map_request_with_state((), f)
}用法:
rust
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:两种合法返回值
map_request.rs:367-386 定义了一个 sealed trait:
rust
// 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 能做"校验 + 短路":
rust
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:核心逻辑
map_request.rs:247-320 宏展开出 1-16 个提取器参数版本。关键段(call 方法内):
rust
// 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
map_response.rs:99-101:
rust
pub fn map_response<F, T>(f: F) -> MapResponseLayer<F, (), T> {
map_response_with_state((), f)
}用法:
rust
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):
rust
// 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:
rust
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 的核心
map_response.rs:229-290 的核心 call:
rust
// 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 类型不必一致。比如:
rust
async fn rewrap<B>(response: Response<B>) -> Response {
let (parts, body) = response.into_parts();
// 把 body 缓冲整个 byte, 再重构成 Response<Body>
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap_or_default();
Response::from_parts(parts, Body::from(bytes))
}输入 Response<B>(inner 的具体 body type),输出 Response<Body>(axum 统一的 Body type)——map_response 不限制你必须返回同样的类型。
这种灵活性让 "响应加工" 能做更深的改造——比如压缩、加密、流式转非流式等。代价是每次都可能触发一次 body 再包装——对大 response 可能慢。
更详细的 map_response 实战
实际生产场景里 map_response 最常用来:给错误响应打上相关性 ID、按 handler 返回的 status 条件设 header、给特定响应加 body 包装。
一个综合示例:
rust
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;
}
// HEAD 请求不带 body - 特殊处理
if method == Method::HEAD {
let (parts, _) = response.into_parts();
return Response::from_parts(parts, Body::empty());
}
response
}
let app = Router::new()
.route("/api/data", get(data_handler))
.layer(map_request(inject_request_id)) // 先塞 RequestId
.layer(map_response(enrich_response)); // 后在响应里读几个技巧:
- 复用前面 middleware 的 extensions:
Extension<RequestId>依赖前面的 map_request - 按 status 条件处理:5xx 完全替换 body、2xx/3xx 只加 header
- HEAD 方法特殊处理:HEAD 响应不应该有 body——map_response 里清空
更复杂的实战:from_extractor + 塞 extensions
from_extractor 的局限是丢弃提取值。想让提取值进入 handler,有两种模式:
模式一:handler 里再提取一次(推荐):
rust
struct CurrentUser(User);
impl FromRequestParts for CurrentUser { /* 提取 */ }
// middleware 层做准入
.route_layer(from_extractor::<CurrentUser>());
// handler 里再提取(和 middleware 重复但必要)
async fn handler(CurrentUser(user): CurrentUser) { ... }第二次提取几乎零成本——因为 FromRequestParts 实现通常把结果缓存到 extensions(看第 7 章 Path 的模式)。但用户侧代码有"重复"——签名里既有 middleware 又有 handler 提取。
模式二:改用 from_fn 塞 extensions:
rust
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:用提取器做准入校验
from_extractor.rs:89-91:
rust
pub fn from_extractor<E>() -> FromExtractorLayer<E, ()> {
from_extractor_with_state(())
}用法:
rust
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:提取 + 丢弃 + 继续
from_extractor.rs:198-232 的核心逻辑:
rust
// 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 的状态机
from_extractor.rs:234-260 的 ResponseFuture 是一个状态机:
rust
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/response 都用一个 Box::pin 包住整个逻辑——简单但多一点分配开销。from_extractor 更精细——状态机拆开、pin 安全、分配更少。为什么 axum 在这三个 helper 里选不同实现?历史原因和精细程度的平衡——from_extractor 是最老的 helper,代码最精心。
何时选哪种
四种 middleware 工具(加 from_fn)都各有适用场景:
| 需求 | 推荐选择 |
|---|---|
| 只改 header / 做前置校验(可短路)、不关心 response | map_request |
| 只在响应上加工(加 header、换格式、wrap errors)、不动 request | map_response |
| 准入校验(有 FromRequestParts 现成)、提取值不用进 handler | from_extractor |
| 既要前置又要后置处理、或逻辑复杂 | from_fn |
需要 poll_ready 或精细 Service 控制 | 原生 Tower Layer |
对"认证"这个典型场景,三种都能做:
rust
// 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 里用 Extension<User> 拿值),把 extractor 当中间件用避免重复。
选 map_request:如果认证只用 Result<Request, Status> 短路、不需要塞 user 进 extensions 给 handler 用。
选 from_fn:如果认证后要在 response 上加点东西(比如 log 认证耗时)、或者 extractor 提取的值要 sideload 到 extensions 给 handler 用、或者代码涉及多步复杂逻辑。
大多数情况下 from_fn 是最通用的,哪种都不清楚就选它。三个专用 helper 存在是为了特定场景下代码更干净。
实战:用 map_response 做全局响应加工
典型场景:所有响应都加一组 security headers。用 map_response 比 tower-http 的 SetResponseHeaderLayer 更灵活——能根据响应内容条件性加 header:
rust
use axum::{middleware::map_response, response::Response};
async fn security_headers(mut response: Response) -> Response {
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 response.status().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));条件性逻辑(看 status 决定 cache-control)是 tower-http::set_header 做不了的——它是无条件 set。map_response 让你写简短的函数处理这种场景。
from_extractor 的 body 消费限制
文档里一句关键警告(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.
如果 from_extractor 用的 extractor 是消费 body 的(比如 Bytes、String、Json<T>),body 会被消费掉——后续的 handler 拿到的 body 是空的。这可能不是你想要的。
典型错误用法:
rust
// ❌ 这会让 handler 拿到空 body
.route_layer(from_extractor::<Json<Payload>>())Json<Payload> 提取时会 body.to_bytes——body 消费完。handler 里声明 Json<Payload> 再提取一次会失败(body 已空)。
正确做法:from_extractor 只搭配 FromRequestParts 提取器(Method、HeaderMap、Uri、自定义的 Parts 提取器)。需要 body 校验的用 map_request 或 from_fn——它们能消费再重构 body 给 handler。
这个约束在 API 层不强制(编译能过)——所以是一个运行时坑。记住:"from_extractor 只用于 parts-only 检查"。
实战:三种 helper 组合使用
同一个 Router 里三种 helper 各司其职的例子:
rust
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 的典型场景:
rust
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 提取器:
rust
// ❌ 编译失败 - map_response 只允许 FromRequestParts
async fn bad(Json(body): Json<Payload>, res: Response) -> Response { ... }Json 是 FromRequest(消费 body)——map_response 拒绝。错误消息是"trait bound FromRequestParts is not satisfied"。修:在 handler 里处理、或改成 from_fn。
坑二:from_extractor 的 body-consuming extractor:
rust
// ❌ 编译能过但运行时 handler 拿不到 body
.route_layer(from_extractor::<Bytes>())
async fn handler(body: Bytes) -> impl IntoResponse { ... } // body 是空坑在编译不检查——需要运行时 debug 发现。修:换 FromRequestParts extractor。
坑三:map_request 里消费 body 但忘重构:
rust
// ❌ 运行时 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:
rust
// ❌ 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):
rust
// ⚠️ 能跑但理解不直观 - 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、发测试请求:
rust
#[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>());
let res = app.oneshot(Request::new(Body::empty())).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
let res_auth = app.clone().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 放一起对比,设计差异一目了然:
rust
// 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 |
| 给请求加 header | map_request | SetRequestHeaderLayer |
| 按 header 做认证 | from_extractor<Auth> | ValidateRequestHeaderLayer |
| 条件性响应加工 | map_response + 自定义逻辑 | 不直接支持 |
| body 缓冲/校验 | map_request + to_bytes | RequestBodyLimitLayer 等 |
选择建议:
- 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 方法手写)
- 零额外堆分配(future 是 stack-pinned)
- 性能稍好但复杂度高
axum 的选择逻辑:from_extractor 是最老的 helper、实现精心;另外三个后加的、BoxFuture 简单优先。未来可能都往 pin_project 迁移——但目前这个差异是历史产物。
对用户侧完全透明——选什么 helper 不影响你的代码。
map_request 的 body 类型约束
看 map_request 的 bound(map_request.rs:265-266):
rust
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,相对 from_fn 的 Request(具体类型)严格很多——map_request 能接受任意 HttpBody。call 里有一段 req.map(Body::new) 把泛型 Body 转成 axum 的 Body——这让用户函数写 Request<B> 仍能工作。
这个设计的好处:map_request 可以挂在 Router 里任何位置——包括在 Body 还没统一成 axum::Body 的地方。from_fn 必须挂在已经统一的位置(Request = Request<Body>)。
实际使用里几乎看不到这个差异——大部分用户在 Router 最外层挂中间件,Body 类型都统一过了。但这个细微灵活性让 map_request 在某些组合场景(nested Router、子 service)里更好用。
三个 helper 的边界情况
case:from_extractor 加了个不 Send 的 state
rust
// 构造时 state 必须 Clone + Send + Sync + 'static
from_extractor_with_state(my_state)::<MyExtractor>()如果 my_state 的某个字段不 Send(比如嵌套了 Rc),会编译失败。修:用 Arc<T> 替换 Rc、或者 restructure state。
case:map_request 的函数参数顺序
rust
// ✅ 对的: 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——能返回和输入类型完全不同的响应:
rust
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_with_state。所有 axum 中间件 helper 都这样提供两版 API。
二、mem::replace + clone 拿走 ready Service:第 12、13 章都讨论过的 Tower 惯用模式。三个 helper 的 call 方法都有这段。
三、宏展开支持 1-16 个提取器:all_the_tuples! 或 impl_service! 宏。和 handler / from_fn 统一。
四、函数体内用 BoxFuture 或 pin_project 状态机:BoxFuture 最简单(from_fn / map_request / map_response 都用),pin_project 状态机更精细(from_extractor 用)。两者性能差异在纳秒级,根据实现优雅度选。
五、Error = Infallible:三者都遵守 axum 的 Service Error 契约——短路路径把 Err 转成 Response。
这些共享模式说明 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:最通用、字符串到任意类型的动态 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:
rust
async fn cache_header<B>(response: Response<B>) -> impl IntoResponse {
let value = match response.status().as_u16() {
200 | 203 | 206 => "public, max-age=300",
301 | 302 => "public, max-age=60",
403 | 404 => "public, max-age=5", // 短缓存让不存在的资源也被 CDN 缓一下
500..=599 => "no-store",
_ => "public, max-age=10",
};
([("cache-control", value)], response)
}
let app = Router::new()
.route("/*", get(handler))
.layer(map_response(cache_header));特点:
- 按 status 精细化 cache-control:2xx 长缓存、3xx 中缓存、4xx 短缓存、5xx 不缓存
- 元组响应 + response:返回
([("cache-control", ...)], response)元组——内置 IntoResponse impl 自动合并 - tower-http 做不到:
SetResponseHeaderLayer不支持按 status 条件——只能无条件 set
这种"基于响应状态的条件加工"是 map_response 相对 tower-http 设置 header 的优势场景。tower-http 覆盖默认行为好、axum helper 覆盖定制行为好——两者互补。
axum 0.9 的 middleware 变化
axum 0.9 对 middleware 有小幅调整:
- IntoResponseFailed 传播到 middleware 层:如果 middleware 的 response 携带 IntoResponseFailed,外层 helper 的修饰逻辑会尊重这个标记——不会覆盖 status
- from_fn 的 F bound 略微放宽:从
F: Clone + Send + 'static调整了 Send 传播规则,让某些闭包更容易通过编译
这些都是兼容性改动——0.8 写的 middleware 升级到 0.9 不需要改。但知道这些细节可能让你理解某些历史项目里看到的 "奇怪写法"——那些往往是 0.8 时代的 workaround。
middleware 和错误处理的集成
中间件的错误处理和第 12 章讲的错误模型无缝衔接:
- middleware 短路 Err:
Result<T, E: IntoResponse>的 Err 自动转 Response——和 handler 一致 - 中间件 panic:外层 CatchPanicLayer 捕获——所有 middleware 类型都一样
- 内部中间件返回 Err:helper 内部把 Err 转 Response、不会暴露给 Service::Error(永远 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 TimeoutLayer + HandleErrorLayer | 请求超时 |
| Request body limit | tower-http RequestBodyLimitLayer | body 大小限制 |
| Concurrency limit | tower::ConcurrencyLimitLayer + HandleErrorLayer | 并发数控制 |
| CatchPanic | tower-http CatchPanicLayer | panic 捕获 |
| Auth | 自定义 from_extractor 或 from_fn | 认证 |
| Rate limit | tower_governor 或自定义 from_fn | 限流 |
| Request ID | 自定义 from_fn | 生成并传播请求 ID |
| Security headers | map_response | 加安全头 |
| Cache-Control | map_response | 按 status 定缓存策略 |
前面七个是 Tower / tower-http 开箱即用的;后面五个通常自己写。典型 axum 应用的 middleware 栈是"3-5 个 tower-http + 3-5 个自定义"——总 8-10 层,每层职责清晰。
测试 middleware 栈的综合模式
完整测试中间件栈需要能控制端到端行为。最佳实践:
rust
mod test_helpers {
pub fn test_app(extra_layers: impl Layer<Route<()>> + Clone) -> Router {
// 测试专用 Router - 只有一个简单 handler
Router::new()
.route("/test", get(|| async { "ok" }))
.layer(extra_layers)
}
}
#[tokio::test]
async fn security_headers_are_added() {
use tower::ServiceExt;
let app = test_helpers::test_app(map_response(security_headers));
let res = app.oneshot(Request::new(Body::empty())).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>());
let res = app.oneshot(Request::new(Body::empty())).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 远)、也没把 Tower 完全包装成 handler-like(warp、salvo 走这条——失去 Tower 生态复用性)。axum 的混合策略让它既能独立开发体验好、又能和 Tower 生态无缝协作。
这种"两种思维并存"也是 axum 中间件一章涵盖内容多的原因——不只一个工具要讲、不只一个心智模型要建立。但一旦两边都学会,axum 的中间件系统就像"瑞士军刀"——小工具做小事、大工具做大事、每种都合适自己的场景。
性能剖析
四种中间件工具的每请求开销差异:
| 工具 | 分配 | vtable 调用 | 额外开销 |
|---|---|---|---|
| 原生 Tower Layer | 0 | 0 | 0 |
| map_request | 1 × Box::pin | 0 | ~50 ns |
| map_response | 1 × Box::pin | 0 | ~50 ns |
| from_extractor | 0 额外 Box(用 pin_project state) | 0 | ~20 ns |
| from_fn | 1 × Box::pin + 1 × BoxCloneSyncService | 1 | ~100 ns |
最便宜是原生 Tower、最贵是 from_fn。from_extractor 用 pin_project 避免了额外 Box 分配——最精细的实现。
实际项目里这些差异忽略不计——handler 业务逻辑(几百微秒到几毫秒)完全覆盖中间件开销。选择主要看代码可读性和开发效率——不是性能。
FAQ:工程决策
Q:项目里所有 middleware 都用 from_fn,会有问题吗?
没问题。功能上完全一致。代价是 1) 每个签名都带 Next、读者不能立刻分辨 middleware 类型 2) 每请求多 100 ns 开销(多一次 Box)。对多数项目这个代价可以忽略,团队统一用 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(state)::<E>()——这时 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 处理)。
从 from_fn 迁移到专用 helper 的重构示例
看一个具体重构。原始 from_fn 代码:
rust
async fn add_cors<B>(request: Request<B>, next: Next) -> Response {
let mut response = next.run(request).await;
response.headers_mut().insert("access-control-allow-origin", "*".parse().unwrap());
response
}这个函数明显只加工 response——用 map_response 更直接:
rust
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 类别,不用深入函数体猜意图。
性能对比实战
写个简单 benchmark 看看四种 middleware 的实际开销:
rust
// 四种 middleware 都做"给 request 加一个 header"的同样工作
// 然后用 criterion benchmark 100 并发 10000 请求
// 假设的相对数据(绝对值因环境不同):
// 原生 Tower Layer: 100 µs/req(base)
// map_request: 100.05 µs (+0.05 µs)
// from_extractor: 100.02 µs (+0.02 µs)
// map_response: 100.05 µs (+0.05 µs)
// from_fn: 100.10 µs (+0.10 µs)差异都在 100 纳秒以下——远小于 handler 本身的业务开销(µs-ms)。这些 helper 的性能差异实际完全可以忽略——选择主要看语法简洁性和意图清晰度。
这个观察也解释了为什么 axum 愿意提供这么多 helper:它们都快到没意义区分——你选 API 最合适的就行。
跨书关联:四种中间件的设计哲学
把第 13、14 章讲的四个中间件工具(map_request、map_response、from_extractor、from_fn)放在 Tower / axum 架构图里:
设计原则是:每个 helper 都在"简洁"和"能力"之间做权衡。能力窄的更简洁、能力广的更通用。用户按需选择——axum 不强制你用最通用的(from_fn),也不逼你用最底层的(Layer)。
这种"分层 API"设计和 Tokio 的 Future / tokio::select! / tokio::spawn 分层类似——不同用户对不同抽象层级有需求、都要满足。
《Hyper 与 Tower:工业级 HTTP 栈》第 5 章讨论过这种"面向使用场景的 API 分层"思想。axum 的中间件系统是那套思想在 HTTP 框架层的具体应用——根据 middleware 常见用法(准入校验、请求加工、响应加工、全能处理)提供专门的 API 简化各自场景。
中间件栈的可视化
把第 13、14 章的所有中间件工具放一起画:
每种颜色区分 middleware 种类:
- 粉:map_response - 仅加工响应
- 黄:from_fn - 通用中间件
- 绿:map_request - 仅改请求
- 蓝:from_extractor - 仅校验
签名就能看出职责——读 Router 代码像读系统架构图。这是 axum 中间件体系的美学。
历史演进
这四个 middleware helper 在 axum 的出现时间:
- from_fn(axum 0.3):最早,一开始就有,解决"不想写 Layer"的问题
- from_extractor(axum 0.4):发现很多 middleware 就是"跑一次 extractor"——提取出来
- map_request / map_response(axum 0.5):观察到大量 middleware 只做单向变换——再专门化
每一版都基于前一版观察使用模式后的提炼:用户想写什么样的中间件?能不能专门针对那种写法做 API 简化? 从 0.3 到 0.5 的演进就是这套观察的结果。
axum 0.6 之后几个 helper API 稳定——新增的类似 helper 主要在 axum-extra(如 axum_extra::middleware::option_layer 等),axum core 保持简洁。
小结
第 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
这种"从粗到细"的演进是 axum 推荐的使用路径——一开始不用过早优化、代码稳定后再按需 specialize。
middleware 选择 rubric
给出一个实战 rubric 帮助中间件选型:
| 条件 | 选择 |
|---|---|
| 只做 request 校验、不需要中间件函数体 | from_extractor(复用现成 extractor) |
| 只改 request / 准入校验、逻辑简短 | map_request |
| 只在响应上加工、不干预请求 | map_response |
| 需要前后都处理或复杂逻辑 | from_fn |
| 需要 poll_ready、复杂 state、发布给其他 tower 项目 | 原生 Tower Layer |
此 rubric 覆盖 95% 场景。剩下 5% 的边界情况(比如"需要同时访问 state 和 inner service"、"需要精细 error mapping")是 tower::Service 的领域——本书第 12 章讨论 HandleError 时已部分涉及。
下一章进入 axum 的运行层——serve 如何监听端口、接受连接、调度 handler。前面章节都是在 axum 的抽象层里讨论,第 15 章开始我们下沉到 tokio / hyper 之上、axum 怎么把 Router(一个 Service)粘到 TCP 监听器上。那一层讨论完整的运行时生命周期——包括优雅关闭。从 14 章的函数式中间件到 15 章的连接层,视角从"请求是一次调用"下降到"请求是一个网络事件"——要转换思维。