Appearance
第13章 from_fn 中间件:提取器 + Next 的函数式模型
前四章讲完请求到响应的完整链路——提取器、handler、响应、错误处理。把一个请求从 hyper 送到 handler 再把响应送回 hyper 的路径上还有一个重要环节:中间件。HTTP 日志、认证、限流、压缩、CORS——都是中间件的工作。
Axum 的中间件有两种风格:
- 原生
tower::Layer + tower::Service:能力最全、也最繁琐。需要实现Servicetrait、处理poll_ready/call、写 Future 类型 axum::middleware::from_fn:把中间件写成一个async fn(request, next) -> Response就完事。适合 80% 的场景
第 14 章会讲 map_request / map_response / from_extractor 等介于两者之间的变体。本章专注 from_fn——它是 axum 最常用的中间件编写方式。
为什么需要"函数式中间件"
直接写 Layer + Service 实现一个"给请求加 header"的中间件大概这样:
rust
// Tower 原生写法(简化)
struct AddHeaderLayer { name: HeaderName, value: HeaderValue }
impl<S> Layer<S> for AddHeaderLayer {
type Service = AddHeader<S>;
fn layer(&self, inner: S) -> Self::Service { /* ... */ }
}
struct AddHeader<S> { inner: S, name: HeaderName, value: HeaderValue }
impl<S> Service<Request<Body>> for AddHeader<S>
where S: Service<Request<Body>, Response = Response<Body>>,
{
type Response = Response<Body>;
type Error = S::Error;
type Future = /* 复杂的 pin_project future 类型 */;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<Body>) -> Self::Future {
req.headers_mut().insert(self.name.clone(), self.value.clone());
self.inner.call(req)
}
}几十行代码——而且还是简单场景。如果要跨 await 访问请求、修改响应,Future 类型会进一步复杂(pin_project、State enum for pending / ready 状态)。
from_fn 让同样的工作变成:
rust
use axum::{middleware, extract::Request, middleware::Next, response::Response};
async fn add_header(mut req: Request, next: Next) -> Response {
req.headers_mut().insert("x-axum-test", "ok".parse().unwrap());
next.run(req).await
}
// 用法
let app = Router::new()
.route("/", get(handler))
.layer(middleware::from_fn(add_header));三行函数体——写起来像写普通 async 函数。内部细节(Layer/Service 包装、Future 类型构造、poll_ready 处理)全被 from_fn 吞掉。
from_fn 的目标不是替代 Tower——生态里大量中间件(TraceLayer、CorsLayer、TimeoutLayer)仍然用原生 Layer + Service——而是给"简单的自定义业务中间件"一个低门槛入口。
middleware 函数的 FnMut 约束细节
from_fn 的 F 要求是 FnMut + Clone + Send + 'static——几乎所有 async fn 都自动满足。但如果函数闭包捕获了某些类型可能不满足:
- 捕获
!Send类型(如Rc<T>)→ 编译失败。解决:用Arc<T>替换 - 捕获
!Clone类型(如TcpStream)→ 函数不 Clone、from_fn拒绝 - 捕获非
'static引用(如本地变量的&str)→'staticbound 失败。解决:move 一份 own 的 String /Arc<str>
大多数情况你写 async fn + 用 State<T> 取依赖,这些问题不会遇到。只有写成闭包 + 手动 move 捕获时会碰到。
设计决策:为什么单独搞一套 from_fn 而不是扩展 tower::Layer
一个合理的反问:Tower 能不能让 Layer 本身就接受 async fn?答案是不能——tower::Layer 是一个通用库,面对各种非 async 场景(比如同步的 Service),无法直接支持 async function 作为一等公民。
axum 选择自己做 from_fn,做了几个针对 HTTP 场景的专门优化:
一、固定 Request/Response 类型:from_fn 写死了 Request 和 Response(axum 的具体类型)。这让 Next 的类型擦除(BoxCloneSyncService)成为可能——泛型擦除依赖固定的输入输出类型。Tower 的通用 Layer 不能这样——它要处理 Service<Req, Response = Res> 的任意 Req/Res。
二、提取器支持:axum 知道自己的 FromRequestParts / FromRequest trait,所以 from_fn 可以自动注入提取器。Tower 不知道这些 trait,不能提供类似支持。
三、Error = Infallible 的统一契约:axum 要求所有 Service Error 是 Infallible(第 12 章)。from_fn 的签名 -> Response 或 -> Result<Response, E: IntoResponse> 自然适配这个契约——E 转成 Response、最终 Service Error 是 Infallible。Tower Layer 不能假设这个契约。
这三点让 from_fn 成为"axum specific"的封装——既有语法便利、又和 axum 的其他机制(提取器、错误处理)无缝配合。牺牲了跨 Tower 项目复用性——但 axum 内部用着舒服。
Next:剩余中间件栈的抽象
from_fn 的中间件函数签名必须有一个 next: Next 参数——Next 是 axum 对"调用中间件栈剩余部分"的抽象。源码在 axum/src/middleware/from_fn.rs:336-350:
rust
// axum/src/middleware/from_fn.rs:336-350
pub struct Next {
inner: BoxCloneSyncService<Request, Response, Infallible>,
}
impl Next {
pub async fn run(mut self, req: Request) -> Response {
match self.inner.call(req).await {
Ok(res) => res,
Err(err) => match err {},
}
}
}Next 内部持有 BoxCloneSyncService<Request, Response, Infallible>——一个类型擦除的 Service 对象,入参是 Request、响应是 Response、Error 是 Infallible。Error = Infallible 是关键——第 12 章讨论过的"axum Service 永远 Infallible"保证让 Next::run 的签名是 async fn run(self, req) -> Response(不是 -> Result<Response, E>)——调用者用起来像普通 async 函数。
match err {} 是 Infallible 的典型用法——空 match 穷尽所有(零个)变体,编译器优化掉这行机器码。
中间件函数拿到 Next 后可以做三件事:
- 不调
next.run(req):短路中间件栈——比如认证失败直接返 401,handler 不执行 - 调
next.run(req).await一次:正常透传——可以在调用前改 req、调用后改 response - 调用多次(罕见):重试逻辑?其实不行——
Next::run按值消费 self,只能调一次。真的要重试需要其他机制
BoxCloneSyncService:为什么需要类型擦除
Next 的 inner 是 BoxCloneSyncService<Request, Response, Infallible>——一个 tower util 提供的"Service trait object"。为什么不用具体类型?
因为 Next 要能被 async 函数按值消费。async fn middleware(req, next: Next) 里 next 的类型必须是具体的——不能是 <S: Service<...>> 这样的泛型(async 函数的泛型参数不能 late-bound)。
类型擦除让 Next 有一个确定的具体类型——代价是一次虚函数调用 + 一次堆分配(box)。在中间件场景下这层开销可以忽略——中间件本身不在纳秒级热路径上。
BoxCloneSyncService 比 BoxService 多要求 Clone + Sync——axum 需要 Service 能被多个请求共享(因为一个 Router 被多个连接并发使用)。Tower 提供这个变体专门为 axum 这类场景。
from_fn 对中间件函数的签名要求
from_fn 文档(from_fn.rs:21-32)明确列出签名约束:
- Be an
async fn.- Take zero or more
FromRequestPartsextractors.- Take exactly one
FromRequestextractor as the second to last argument.- Take
Nextas the last argument.- Return something that implements
IntoResponse.
和第 5 章讲的 handler 签名几乎一样——除了最后多一个 Next 参数。这不是巧合:middleware 和 handler 在 axum 眼里是同一种东西——都是"处理 Request 产出 Response 的 async fn"——差别只在 middleware 能透传给下一层。
合法签名举例:
rust
// 最简:只有 req + next
async fn m1(req: Request, next: Next) -> Response;
// 带前置提取器:可以提取 headers / method / uri 等
async fn m2(method: Method, headers: HeaderMap, req: Request, next: Next) -> Response;
// 返回 Result:Err 也要 IntoResponse
async fn m3(headers: HeaderMap, req: Request, next: Next) -> Result<Response, StatusCode>;
// 最后一个提取器消费 body(req 是 FromRequest<S>)
async fn m4(method: Method, req: Request, next: Next) -> Response; // req 消费 body注意规则 3:"second to last 是 FromRequest"——因为 body 只能消费一次(第 6 章讨论过)。如果 middleware 不想消费 body,第 second-to-last 参数通常用 Request(自身实现 FromRequest 的恒等 impl)——这等于"拿到完整 request 但不动 body"。
FromFn 的 Service 实现
中间件函数经过 from_fn(f) 变成 FromFnLayer<F, S, T>,layer(inner) 生成 FromFn<F, S, I, T>——最终的 Service。核心 impl 在 from_fn.rs:254-318(宏展开 1-16 个前置参数版本):
rust
// axum/src/middleware/from_fn.rs:258-316 (简化, 三参数版本)
impl<F, Fut, Out, S, I, T1, T2, T3> Service<Request>
for FromFn<F, S, I, (T1, T2, T3)>
where
F: FnMut(T1, T2, T3, Next) -> Fut + Clone + Send + 'static,
T1: FromRequestParts<S> + Send,
T2: FromRequestParts<S> + Send,
T3: FromRequest<S> + Send,
Fut: Future<Output = Out> + Send + 'static,
Out: IntoResponse + 'static,
I: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
I::Response: IntoResponse,
I::Future: Send + 'static,
S: Clone + Send + Sync + 'static,
{
type Response = Response;
type Error = Infallible;
type Future = ResponseFuture;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
let not_ready_inner = self.inner.clone();
let ready_inner = std::mem::replace(&mut self.inner, not_ready_inner);
let mut f = self.f.clone();
let state = self.state.clone();
let (mut parts, body) = req.into_parts();
let future = Box::pin(async move {
// 依次调用 FromRequestParts 提取器
let T1 = match T1::from_request_parts(&mut parts, &state).await { ... };
let T2 = match T2::from_request_parts(&mut parts, &state).await { ... };
// 重建 Request, 调用最后一个 FromRequest 提取器
let req = Request::from_parts(parts, body);
let T3 = match T3::from_request(req, &state).await { ... };
// 包装 inner 成 Next
let inner = BoxCloneSyncService::new(MapIntoResponse::new(ready_inner));
let next = Next { inner };
// 调用用户 middleware 函数
f(T1, T2, T3, next).await.into_response()
});
ResponseFuture { inner: future }
}
}逐段拆解。
poll_ready 转发
self.inner.poll_ready(cx)——简单转发给 inner。from_fn 本身不引入 back-pressure(中间件函数是 async fn,没有 ready 概念),内层 Service 的 ready 状态直接向上传。
clone-and-replace 模式
第 12 章讨论过的 Tower 惯用模式——let ready_inner = std::mem::replace(&mut self.inner, clone)。ready_inner 进入 async block 被用(处于 ready 状态,call 合法),self.inner 留的是未 ready 的 clone 副本。
提取器顺序
前 N-1 个参数(FromRequestParts)通过 from_request_parts(&mut parts, &state) 依次提取;最后一个(FromRequest)通过 from_request(req, &state) 消费整个请求——和第 5 章 handler 完全一样。state 从 self.state.clone() 取(from_fn_with_state 提供的 state),没有 state 时是 ()。
MapIntoResponse:让 inner 的 Response 类型归一
MapIntoResponse::new(ready_inner) 是 axum 内部的 adapter——把一个 Service<Response = T: IntoResponse> 包装成 Service<Response = Response>。为什么需要?
因为 Next 的 inner 类型是 BoxCloneSyncService<Request, Response, Infallible>——写死了 Response 类型是 Response。但 Layer 包装的 inner Service 的 Response 可能是其他 IntoResponse 类型(比如 Service<Response = String>)。MapIntoResponse 在 call 时做 response.into_response() 转换,让 inner 能塞进 BoxCloneSyncService。
这是"类型擦除 + 自动适配"的典型手法——不让用户感知内部的类型归一。
ResponseFuture 的结构
from_fn.rs:367-377:
rust
pub struct ResponseFuture {
inner: BoxFuture<'static, Response>,
}
impl Future for ResponseFuture {
type Output = Result<Response, Infallible>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.inner.as_mut().poll(cx).map(Ok)
}
}inner 是 BoxFuture<'static, Response>——直接返回 Response 的 future。poll 里 .map(Ok) 把 Response 包装成 Result<Response, Infallible> 适配 Service trait——和第 5 章 HandlerService 用同样手法。
整条 from_fn 的内部数据流:
from_fn_with_state:注入共享 state
from_fn 不接受 state——from_fn_with_state(state, f) 才行。区别是后者让 middleware 函数能用 State<T> 提取器:
rust
async fn auth_middleware(
State(db): State<PgPool>, // 从 state 提取
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
// 用 db 做认证
let user = db.check_token(/* ... */).await.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(next.run(request).await)
}
let app = Router::new()
.route("/protected", get(handler))
.route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware))
.with_state(state);state.clone() 传给 from_fn_with_state——middleware 的 state;另一个 state 传给 .with_state——handler 的 state。两份必须一致。
from_fn 是 from_fn_with_state((), f) 的快捷方式(from_fn.rs:114-116)——内部 state 是 ()、提取器 bound 是 FromRequestParts<()>。所以 from_fn 能用 Method / HeaderMap 这种不依赖 state 的提取器,不能用 State<T>。
何时用 from_fn、何时用 tower::Layer
两种风格的适用场景:
| 场景 | from_fn | tower::Layer |
|---|---|---|
| 写一个简单的业务中间件 | ✓ | 过度工程 |
需要 poll_ready 的真正背压逻辑 | ✗ | ✓ |
| 需要精细控制 Service 内部状态 | ✗ | ✓ |
| 想给 axum 之外的项目用(其他 tower 用户) | ✗ | ✓ |
| 多个 Router 反复用同一个中间件 | 都行 | 更合适 |
| 需要从中间件函数内访问类似 async fn 的控制流 | ✓ | 麻烦 |
| 性能极度敏感(每纳秒都算) | ✗(多一次 box 分配) | ✓ |
| 需要 FromRequestParts 提取器 | ✓ | 自己写要多写很多代码 |
简单判断:如果你只是想"在 handler 前后插点逻辑",用 from_fn。如果要做"真正的 Service 改造"(背压、流量控制、连接级状态),用 Layer。
from_fn 的开销:每次请求一次 BoxCloneSyncService::new + 一次 Box::pin——几十纳秒到一百纳秒。对 millisecond 级的 handler 业务完全可忽略;对 sub-microsecond 热路径可能显著。
中间件的叠加与执行顺序
多个 from_fn 叠加时的执行顺序是下一个必须理解的概念。看这段代码:
rust
let app = Router::new()
.route("/", get(handler))
.layer(from_fn(outer_mw)) // 最后加
.layer(from_fn(middle_mw))
.layer(from_fn(inner_mw)); // 最先加Tower 的 .layer(X) 是"把 X 包在当前 Service 外面"——即后添加的 Layer 在请求流向上更靠外。所以上面的执行顺序是:
text
请求 → outer_mw → middle_mw → inner_mw → handler
响应 ← outer_mw ← middle_mw ← inner_mw ← handler每个中间件的代码结构都是"next.run 前做点事、next.run 后做点事"——这形成了洋葱模型:
每个中间件的"before"代码在 next.run 之前运行、"after"代码在 next.run 之后——Before 按最外到最里顺序、After 按最里到最外逆序。这和其他框架(Express、Koa、Django middleware)的经典洋葱模型一致——但在 axum 里它是类型系统自然派生的结果,不是额外约定。
顺序在生产里的影响:
- 认证在最外:请求没认证通过就别让其他中间件浪费 CPU
- 日志在最外或最外之次:记录完整生命周期的时间
- rate limit 在认证之后:rate limit 通常按用户/API key 计数,需要先认证拿到身份
- 压缩(compression)放内:压缩后的数据经过的中间件越少越好
这些是经验原则——没有框架强制,用户自己决定 Layer 顺序。错误的顺序不会编译失败但会行为不对。
实战一:认证中间件
from_fn 的规范场景就是认证——检查请求 header、拿到用户、决定放行或拒绝:
rust
use axum::{
extract::{Request, State},
http::{HeaderMap, StatusCode},
middleware::{self, Next},
response::Response,
};
async fn auth(
State(state): State<AppState>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let token = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let user = state.auth_service.verify_token(token)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// 把 user 塞进 request extensions, 后续 handler 可以提取
request.extensions_mut().insert(user);
Ok(next.run(request).await)
}
let app = Router::new()
.route("/protected", get(protected_handler))
.route_layer(middleware::from_fn_with_state(state.clone(), auth))
.with_state(state);
// handler 里用 Extension<User> 拿到认证结果
async fn protected_handler(Extension(user): Extension<User>) -> impl IntoResponse {
format!("hello {}", user.name)
}几个要点:
一、短路即 Err:返回 Err(StatusCode::UNAUTHORIZED) 直接变 401 响应——因为 StatusCode impl IntoResponse。Result<Response, StatusCode> 的两边都能变 Response,这让 ? 无缝工作。
二、往 extensions 塞 user:handler 通过 Extension<User> 拿到——中间件和 handler 之间通过 extensions 传递类型化数据。第 11 章讲过这种模式。
三、route_layer vs layer:route_layer 只作用到指定路由、不影响 fallback / nest 等路由;layer 作用于整个 Router 包括 fallback。认证通常用 route_layer——让 fallback(比如 404 页面)不被认证拦截。
实战二:请求日志中间件
结构化日志通常用 tower_http::trace::TraceLayer——但有时想要更定制化的 log,用 from_fn 写更灵活:
rust
use axum::{
extract::Request,
middleware::Next,
response::Response,
};
use std::time::Instant;
use tracing::{info, info_span, Instrument};
async fn log_request(request: Request, next: Next) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let span = info_span!("request", %method, %uri);
async move {
let start = Instant::now();
info!("started");
let response = next.run(request).await;
let elapsed = start.elapsed();
info!(status = response.status().as_u16(), elapsed_ms = elapsed.as_millis(), "done");
response
}
.instrument(span)
.await
}亮点:
tracing::info_span!+.instrument:span 覆盖next.run的整个执行——handler 里的 tracing 事件自动带上 request 的 span context(method、uri 作为 tag)- 时间测量:
Instant::now+elapsed,记录处理时间 - 不塞不必要的字段:只 log 开始/结束和状态码。如果每个字段都塞会让日志容量暴涨——按监控 / 排查需要精选
这比用 TraceLayer 定制化更灵活——但 TraceLayer 自动处理 span propagation、和 OpenTelemetry 集成等高级功能。生产里建议:轻量自定义用 from_fn,完整可观测性用 TraceLayer + 自定义 span 配置。
实战三:基于 state 的 rate limit
rate limit 通常用 tower::limit——但某些精细策略(按 API key、按 endpoint)需要自定义:
rust
use std::sync::Arc;
use dashmap::DashMap;
use std::time::{Duration, Instant};
#[derive(Clone)]
struct RateLimitState {
windows: Arc<DashMap<String, (Instant, u32)>>,
max_per_minute: u32,
}
async fn rate_limit(
State(limit): State<RateLimitState>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let key = headers.get("x-api-key")
.and_then(|v| v.to_str().ok())
.map(String::from)
.ok_or(StatusCode::UNAUTHORIZED)?;
let now = Instant::now();
let mut entry = limit.windows.entry(key.clone()).or_insert((now, 0));
// 一分钟重置窗口
if now.duration_since(entry.0) >= Duration::from_secs(60) {
*entry = (now, 0);
}
if entry.1 >= limit.max_per_minute {
return Err(StatusCode::TOO_MANY_REQUESTS);
}
entry.1 += 1;
drop(entry); // 显式释放 DashMap lock, 避免跨 await 持有
Ok(next.run(request).await)
}关键细节:
一、DashMap 而非 std::sync::Mutex<HashMap>:DashMap 是分片锁 HashMap,并发读写性能好。Mutex<HashMap> 的锁会串行化所有访问——rate limit 中间件每个请求都会用,全局锁是瓶颈
二、drop(entry) 显式释放:entry 是 DashMap 的 guard(包含锁),跨 await 持有会让 future 非 Send(编译失败)。显式 drop 在 await 前让锁释放
三、内存可能无限增长:entry.or_insert(...) 对新 key 创建条目但从不清理。生产里需要定期扫描旧条目删掉(windows 上次访问时间 > 某阈值)——tokio::spawn 一个后台任务做 GC
这种 sliding window rate limit 仅是示例,真实 production 推荐用 tower_governor / tower::load_shed 或配合 Redis 做分布式 rate limit。
分布式场景的 rate limit
单机 DashMap 只能限单个进程的流量——生产里多实例部署需要共享状态。典型方案是 Redis:
rust
async fn rate_limit_redis(
State(redis): State<redis::aio::MultiplexedConnection>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let key = extract_key(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
let redis_key = format!("ratelimit:{key}");
let mut conn = redis.clone();
// INCR + EXPIRE 原子组合(用 Lua script 做真正原子)
let count: u32 = redis::cmd("INCR").arg(&redis_key).query_async(&mut conn).await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if count == 1 {
let _: () = redis::cmd("EXPIRE").arg(&redis_key).arg(60).query_async(&mut conn).await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
if count > 100 {
return Err(StatusCode::TOO_MANY_REQUESTS);
}
Ok(next.run(request).await)
}几个生产要点:
- 用 Lua script 做原子:上面 INCR + EXPIRE 非原子、race condition 时 TTL 可能永远不设上——推荐写一个 Lua script 原子处理
- 失败路径行为:Redis 宕掉时 middleware 是拒绝(安全优先)还是放行(可用性优先)?生产需要明确策略。默认建议放行——否则 Redis 单点故障让整个服务 429
- 连接池:middleware 每次用 Redis,连接池大小要够。
bb8-redis/deadpool-redis是常见选择 - 本地 + 分布式混合:结合本地(DashMap)做粗限(防爆发流量让 Redis)+ 分布式(Redis)做精限——混合方案更稳
这些复杂性说明:rate limit 是用 tower_governor 等成熟库的典型场景——不必自己写一套。from_fn 快速原型时手写、生产里换成库。
from_fn 的历史演进
from_fn 在 axum 不同版本的演化:
- axum 0.3 之前:没有 from_fn。所有中间件都要手写 Layer/Service——门槛高,劝退很多想自定义中间件的用户
- axum 0.3:引入
middleware::from_fn——最早版本只支持单一(req, next)签名,不支持提取器 - axum 0.5+:加入提取器支持——signature 可以带 1-16 个 FromRequestParts + 一个 FromRequest。让 middleware 能用
Method/HeaderMap这些 handler 级别的便利 - axum 0.6:
from_fn_with_state加入——让 middleware 能访问 Router state - axum 0.7+:稳定、小改进(类型 bound 简化、错误消息改善)
每一版 from_fn 都在让中间件编写门槛更低、能力更接近 handler。到 0.8 时 from_fn 的心智模型和 handler 完全一致——signature 像 handler、能用提取器、? 错误处理一样——唯一差别是多了个 Next。
对比其他框架,axum 的 from_fn 接近 Koa / Express 的 middleware 心智模型:
javascript
// Koa
app.use(async (ctx, next) => {
// before
await next();
// after
});rust
// axum from_fn
async fn mw(req: Request, next: Next) -> Response {
// before
let response = next.run(req).await;
// after
response
}两者神似。差异在类型系统——axum 有提取器和 state、Koa 用 ctx 对象聚合一切。axum 用类型表达的东西 Koa 用命名对象聚合——各有优劣。
from_fn 的几种高级用法
一、按条件不调 next:authorization、feature flag、maintenance mode 等场景:
rust
async fn maintenance(request: Request, next: Next) -> Response {
if is_maintenance_mode() {
(StatusCode::SERVICE_UNAVAILABLE, "under maintenance").into_response()
} else {
next.run(request).await
}
}二、给响应强制加 header(覆盖 handler 自己设的):
rust
async fn force_security_headers(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
let h = response.headers_mut();
h.insert("x-frame-options", HeaderValue::from_static("DENY"));
h.insert("x-content-type-options", HeaderValue::from_static("nosniff"));
response
}三、按响应状态码做不同后处理:
rust
async fn error_reporter(request: Request, next: Next) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let response = next.run(request).await;
if response.status().is_server_error() {
tracing::error!(%method, %uri, status = %response.status(), "5xx response");
}
response
}四、改 request body(比如在 body 前加 header 签名):
rust
async fn verify_hmac(request: Request, next: Next) -> Result<Response, StatusCode> {
let (parts, body) = request.into_parts();
let bytes = axum::body::to_bytes(body, 10 * 1024 * 1024).await
.map_err(|_| StatusCode::PAYLOAD_TOO_LARGE)?;
let expected_hmac = parts.headers.get("x-signature")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
if !verify_hmac_sig(&bytes, expected_hmac) {
return Err(StatusCode::UNAUTHORIZED);
}
// body 验证通过, 重构 request 给 handler
let request = Request::from_parts(parts, Body::from(bytes));
Ok(next.run(request).await)
}五、条件性应用中间件(运行时决定):这用 from_fn 做不优雅——应该用 Router::layer 配合 cfg 或者用 MapOr 之类的组合子。from_fn 内部条件更适合"每个请求的快速判断",不适合"全局 on/off"。
六、并行 middleware 分支:某些场景想"并行做多件事、都完成后才继续"(比如同时检查 authz 和 quota):
rust
async fn parallel_checks(
State(services): State<Services>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_check = services.auth.verify(&headers);
let quota_check = services.quota.check(&headers);
// join! 并行两个 future
let (auth_res, quota_res) = tokio::join!(auth_check, quota_check);
auth_res.map_err(|_| StatusCode::UNAUTHORIZED)?;
quota_res.map_err(|_| StatusCode::TOO_MANY_REQUESTS)?;
Ok(next.run(request).await)
}tokio::join! 同时驱动两个 future——比串行快一半。做多个独立后端调用时(比如同时查用户表和权限表)值得用。但要注意 join 不是无限并发——future 是在同一个任务里,如果都是 CPU 密集的阻塞操作还是串行的。I/O 密集的调用(网络 RPC、数据库查询)才有加速效果。
常见 pattern 小结
几个 axum 生态常见的 from_fn middleware:
| middleware | 典型签名 | 常用场景 |
|---|---|---|
auth | (State<S>, HeaderMap, Request, Next) -> Result<Response, StatusCode> | 登录验证 |
log_request | (Request, Next) -> Response | 请求日志 |
rate_limit | (State<S>, HeaderMap, Request, Next) -> Result<Response, StatusCode> | 限流 |
cors_preflight | (Method, Request, Next) -> Response | OPTIONS 请求短路处理 |
request_id | (Request, Next) -> Response | 生成 / 传播 X-Request-Id |
feature_flag_gate | (State<S>, HeaderMap, Request, Next) -> Response | 按用户开关 feature |
request_body_size_limit | (Request, Next) -> Result<Response, StatusCode> | body 大小预检 |
这些 middleware 中大部分都能用 tower-http 或其他成熟库——但手写了解原理对调试很重要。生产项目的中间件栈通常是"2-3 个库中间件 + 3-5 个自己的 from_fn 中间件"混用。
中间件和错误处理的协作
from_fn 中间件返回 Result<Response, E> 时,E 必须 IntoResponse——短路走 E 的 into_response 产出响应。这和 handler 的错误模型一致,但有几个细节:
一、Err 分支不进入后续中间件:短路直接返给更外层。inner_mw 的 Err 不会经过 middle_mw 的"after"代码——洋葱被中途截断。
二、中间件的 Err 也能经过 HandleErrorLayer:但只能处理 Service::Error,而 from_fn 的 Error 类型是 Infallible(短路时 Err 已经变成 Response)。所以 HandleErrorLayer 不会捕获 from_fn 中间件的短路——那已经是 Response 了。
三、panic 跟普通 async fn 一样:需要外层 CatchPanicLayer 捕获。中间件 panic 和 handler panic 在 axum 眼里一样——都被 CatchPanic 统一处理。
这意味着:from_fn 中间件的错误模型等同于 handler——Result<T, E: IntoResponse> + ?、没 panic 就不会断连。正是这层一致性让中间件代码读起来和 handler 一样熟悉。
rust
async fn auth(req: Request, next: Next) -> Result<Response, AppError> {
let user = verify(&req)?; // ? 把 AppError 短路成 Response
let response = next.run(req).await;
Ok(response)
}AppError: IntoResponse 就行,其他都一样。
实战四:AI 应用的 middleware 模式
LLM 应用里典型的 middleware 需求:
一、API key 认证 + 用户上下文:
rust
async fn auth_and_enrich(
State(auth): State<AuthService>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let key = headers
.get("x-api-key")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let user_ctx = auth.verify(key).await.map_err(|_| StatusCode::UNAUTHORIZED)?;
// 注入用户上下文到 extensions, handler 可提取
request.extensions_mut().insert(user_ctx);
Ok(next.run(request).await)
}二、Token 配额预检查:
rust
async fn check_quota(
State(quota): State<QuotaService>,
Extension(user): Extension<UserContext>,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
if !quota.has_quota(user.id, user.tier).await {
return Err(StatusCode::TOO_MANY_REQUESTS);
}
Ok(next.run(request).await)
}注意这里用 Extension<UserContext>——依赖上一个 auth_and_enrich 已经塞进 extensions。route_layer 的顺序要保证 auth 在 quota 之外(执行时 auth 先跑、quota 后跑)。
三、计费后处理:handler 执行后读取 token 使用量写账单。这不是 from_fn 的典型 pattern,因为需要 "handler 完成后异步上报"——建议 fire-and-forget:
rust
async fn billing_record(
State(billing): State<BillingService>,
Extension(user): Extension<UserContext>,
request: Request,
next: Next,
) -> Response {
let response = next.run(request).await;
// 从响应 extensions 读 token 用量(handler 返回时设置)
if let Some(usage) = response.extensions().get::<TokenUsage>() {
let billing = billing.clone();
let user_id = user.id;
let usage = *usage;
tokio::spawn(async move {
billing.record(user_id, usage).await;
});
}
response
}关键:billing 的写入用 tokio::spawn 异步执行——不阻塞响应返回给客户端。如果在同步路径上等 billing 写完再返响应,客户端延迟会被拖慢。fire-and-forget 在 LLM 场景常用——token 用量统计、审计日志、通知等都适用。
这三个中间件叠加是 LLM API 的标准栈——/chat 这样的 endpoint 会自动获得"认证 → 配额 → 计费"的完整处理流程,每个环节都是独立的 from_fn,可单独测试和替换。
实战五:请求 ID 追踪
跨服务 tracing 需要 "request id"——每个请求打上唯一 ID,log / downstream call 都带上,方便事后定位。这是 from_fn 的经典用例:
rust
use uuid::Uuid;
async fn request_id(mut request: Request, next: Next) -> Response {
// 如果 request 已带 id(来自上游 gateway),用它;否则生成新的
let id = request
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(String::from)
.unwrap_or_else(|| Uuid::new_v4().to_string());
// 塞进 request extensions,handler 能提取
request.extensions_mut().insert(RequestId(id.clone()));
// 打开 tracing span, 让这条 request 的所有 log 都带 request_id
let span = tracing::info_span!("request", request_id = %id);
let mut response = async move { next.run(request).await }
.instrument(span)
.await;
// 把 id 写入响应 header, 客户端调试时能看到
response.headers_mut().insert(
"x-request-id",
HeaderValue::from_str(&id).unwrap(),
);
response
}
#[derive(Clone, Debug)]
pub struct RequestId(pub String);几个工程考虑:
- 复用上游的 id:如果请求已经从负载均衡/网关过来带了 X-Request-Id,保留它——让整条链路一致
- 塞 Extension:handler 里
Extension<RequestId>能取到,用于业务日志 - set span:
.instrument(span)让 handler 和下游 middleware 的 tracing event 都自动带上 request_id field——不用每条 log 手动写 - 响应也返回:客户端在调试时能看到 id——bug 报告里带上就能服务端精确定位那条请求
这种中间件是生产 axum 应用的最低标配——和 TraceLayer 配合几乎能满足所有可观测性需求。
测试 from_fn 中间件
from_fn 中间件的单元测试有两种层次。
一、集成测试:挂到 Router 里端到端跑:
rust
#[tokio::test]
async fn auth_returns_401_without_token() {
use axum::{body::Body, http::Request};
use tower::ServiceExt;
let state = AppState::default();
let app = Router::new()
.route("/protected", get(|| async { "ok" }))
.route_layer(middleware::from_fn_with_state(state.clone(), auth))
.with_state(state);
let res = app
.oneshot(Request::builder().uri("/protected").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}优点:测试完整链路;缺点:要构造整个 Router 状态,测试大而臃肿。
二、单元测试:直接调用中间件函数:
rust
#[tokio::test]
async fn auth_inserts_user_into_extensions() {
use tower::util::BoxCloneSyncService;
let state = AppState::default();
let request = Request::builder()
.header("authorization", "Bearer valid_token")
.body(Body::empty())
.unwrap();
// 构造一个 Next 指向"直接返回 200 + extensions"的 inner Service
let inner = tower::service_fn(|req: Request| async move {
// 把 req.extensions::<User> 的存在作为 assert
let has_user = req.extensions().get::<User>().is_some();
assert!(has_user);
Ok::<_, Infallible>(Response::new(Body::from("ok")))
});
let next = Next { inner: BoxCloneSyncService::new(inner) };
let headers = HeaderMap::new(); // 实际从 request 拿
let result = auth(State(state), headers, request, next).await;
assert!(result.is_ok());
}直接构造 Next 调用函数——更快、更单元化。缺点:要 import Next 的构造器(axum 里它是 private),这种测试通常只在 axum 自己的代码里做。应用侧多用集成测试。
三、用 mock_next pattern:
rust
// 自己写个 helper 模拟 Next
fn mock_next(response: Response) -> Next {
let inner = tower::service_fn(move |_: Request| {
let response = response.clone();
async move { Ok::<_, Infallible>(response) }
});
Next { inner: BoxCloneSyncService::new(inner) }
}把这个 helper 抽成测试工具——后续所有中间件测试都可以用 mock_next 生成一个"确定响应"的 Next 去测中间件行为。推荐项目里放一个 test_helpers::mock_next,所有 middleware 的测试都基于它。
调试 from_fn 中间件
写中间件常遇到的几种调试场景。
一、断言 middleware 真的挂上了:给 middleware 体内加 tracing::debug! 或 eprintln!,发一次测试请求看日志有没有这条 message。没有就说明 middleware 没被调用——检查 route_layer vs layer、检查 Layer 顺序。
二、中间件 panic 但看不到错误:panic 发生在 async future 里,不带 stack trace 直接显示。生产里要配 CatchPanicLayer + panic = "unwind"。调试时 RUST_BACKTRACE=1 能让 stack trace 显示——但 async 的 stack trace 可读性一般。
三、next.run 卡住不返回:通常是 handler 本身慢——middleware 本身很少有长时间操作。用 tokio-console 或 tracing::debug! 前后定位。
四、state 类型不匹配编译错:错误消息里expected State<A>, found State<B>——检查 from_fn_with_state 的 state 类型和 .with_state 的一致。
五、"Future cannot be sent between threads" 编译错:通常是中间件体内跨 await 持有了 !Send 的东西——std::sync::MutexGuard、Rc<T>、RefCell。换成 tokio::sync::Mutex 或在 await 前 drop guard。
Next 自己也是 Service
from_fn.rs:352-364:
rust
impl Service<Request> for Next {
type Response = Response;
type Error = Infallible;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
self.inner.call(req)
}
}Next 实现 Service——意味着它能作为 Tower Service 使用。这让 middleware 函数里能用其他 Tower 组合工具:
rust
async fn retry_middleware(request: Request, next: Next) -> Response {
use tower::ServiceExt;
let (parts, body) = request.into_parts();
// 注意: 不能真的 retry, 因为 next 只能被 consume 一次、body 也只能消费一次
// 但可以对 next 做 Tower 的其他操作
let mut next_service = next; // Next 是 Service
// ...
}实际 Next::run 更简单好用——大多数场景用 run 就够。Service impl 主要是为了让 Next 能 compose 进 Tower 生态(比如 next.map_response(...)——但 axum 自己没大量用这个)。
为什么 Next::run 按值消费 self
一个值得注意的设计细节:Next::run(self, req) 按值消费 self,不是 &mut self。这意味着:
- 一个 Next 只能 run 一次:调用后 self 被消耗、不能重用
- 中间件不能重试:想"失败了再调一次 next"——需要先 clone Next
这个设计限制匹配了 Service::call 的语义——call 消费 readiness token。Next 内部就是一个 Service,让 run 按值就是明确"这次 call 用掉了 readiness"。如果要重试,要先 Next::clone(Next 实现 Clone)、然后两个独立 Next 各 run 一次——但每次都要重新构造 request(body 消费过了)。
这种设计让 run 的语义直白——用户看 next.run(req).await 就知道 next 不能再用了,不会误操作。
性能调优建议
中间件栈的性能有几个常见关注点:
一、避免每请求都 clone 大对象:中间件函数是 FnMut + Clone——每次请求 clone 一份。如果你捕获了一个大数据结构,clone 可能贵。解决:用 Arc<T> 包装大数据,clone 只是 Arc::clone 零成本。
二、避免跨 await 持有锁:std::sync::MutexGuard 不是 Send,持有时 future 变成 !Send,编译失败。即使用 tokio::sync::Mutex(可以跨 await),长时间持有也是性能问题——所有请求被串行化。lock().await 前把要做的活准备好、拿到锁后快速完成、立即 drop。
三、减少每请求的分配:from_fn 本身每请求有两次 box 分配(Box::pin + BoxCloneSyncService::new)——这是必然开销。额外的分配(format! / to_string / Vec::new)尽量合并或用 Cow。
四、路由 layer 而不是全局 layer:认证 layer 如果只有部分 endpoint 需要,用 route_layer 限制作用域——非认证 endpoint 不走这个 middleware、省一段开销。
五、精确监控每个中间件的耗时:tracing 的 #[instrument] 能自动计时 async 函数——加到中间件函数上,就能在日志里看到每个中间件的 p50 / p99。生产里这个指标是定位性能问题的关键。
中间件栈的可视化
最后把一个完整生产 Router 的中间件栈画出来:
三种颜色区分三类中间件:
- 绿色(Tower 生态):tower / tower-http 提供,开箱即用
- 粉色(axum from_fn):自己写的业务中间件
- 红色(错误处理):CatchPanic + HandleError,保证响应总产生
这种栈是 axum 生产最常见的形态——10 层左右的 Layer,每层职责清晰,开发者看配置就知道请求会经过什么流程。
常见陷阱
陷阱一:middleware state 和 Router state 类型不一致:
rust
// ❌ from_fn_with_state 给的 state 和 Router::with_state 给的不一致
let app = Router::new()
.route("/", get(handler))
.route_layer(middleware::from_fn_with_state(middleware_state, mw)) // MiddlewareState
.with_state(app_state); // AppState
// handler 里 State<AppState> 是从 app_state 来
// middleware 里 State<MiddlewareState> 是从 middleware_state 来两个 state 是完全独立的——一个给中间件、一个给 handler。但这容易造成混乱:想让 middleware 和 handler 共享 state 时必须传同一个实例(或从同一个派生)。
推荐模式:middleware 的 state 就是 Router state 的一部分——用 FromRef 或就是 clone 一份:
rust
let shared = AppState { /* ... */ };
let app = Router::new()
.route("/", get(handler))
.route_layer(middleware::from_fn_with_state(shared.clone(), mw))
.with_state(shared);陷阱二:body 消费后不能再 next.run:
rust
// ❌ 消费了 body 就不能 next.run(原 request)
async fn wrong(mut request: Request, next: Next) -> Response {
let bytes = Bytes::from_request(request, &()).await.unwrap(); // 消费 body
// request 已经被 move, 不能再用
// next.run(???) <- 没 request 可用了
next.run(/* ??? */).await
}如果想 peek body 然后继续——必须先 buffer、读完再重构 request:
rust
async fn peek(request: Request, next: Next) -> Response {
let (parts, body) = request.into_parts();
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
// 用 bytes 做检查
let request = Request::from_parts(parts, Body::from(bytes)); // 重构
next.run(request).await
}注意这让 body 整个被加载到内存——对大 body(文件上传)不合适。
陷阱三:next.run 忘了 .await:
rust
// ❌ 忘了 await, 返回的是 Future 不是 Response
async fn bad(req: Request, next: Next) -> Response {
next.run(req) // Future<Output = Response>
// 忘了 .await, 类型错
}编译会失败——但错误消息可能不直观(提示类型不匹配)。一般都能快速察觉。
陷阱四:route_layer 和 layer 搞混:route_layer 只包路由、layer 包整个 Router(包括 fallback)。认证中间件通常用 route_layer——避免 fallback 404 也需要认证。CORS / trace 通常用 layer 覆盖全部。
陷阱五:middleware 里 spawn task 但不等待:
rust
async fn mw(req: Request, next: Next) -> Response {
tokio::spawn(async { something().await; }); // fire-and-forget 不 await
next.run(req).await
}fire-and-forget 可以——但如果 spawn 的 task panic,log 里只看到 "task panicked" 没有 request 上下文。建议 spawn 内用 tracing::error! 带上足够上下文。
一个微妙点:FnMut 而非 FnOnce
FromFn::call 的 bound 是 F: FnMut(...) -> Fut + Clone——不是 FnOnce。但 middleware 函数体里通常用 next.run(req).await——run 按值消费 next——这是 FnOnce 的消费 pattern。两者如何共存?
关键在 self.f.clone():
rust
// 简化源码
fn call(&mut self, req: Request) -> Self::Future {
let mut f = self.f.clone(); // clone 一份 f 给本次 call 用
// ...
let future = Box::pin(async move {
// ...
f(T1, T2, T_last, next).await.into_response()
});
ResponseFuture { inner: future }
}每次 call 都 clone 一份 f——clone 的副本在本次 future 里被 FnOnce 式消费。FnMut + Clone 组合起来实际上是"每次 call 独立、可以消费内部状态、但下次 call 能拿新副本"。这个模式在 Rust async 生态里很常见——Tower、axum 都用。
对用户来说:middleware 函数里可以自由 next.run(req).await(消费)、可以捕获值 move 进 async block——只要函数本身 Clone(通常 async fn 都自动 Clone,除非捕获了 !Clone 类型)。
from_fn 的限制
几个 from_fn 的边界:
一、无 poll_ready 钩子:middleware 函数是 async fn,poll_ready 直接转发 inner——不能自己做 back-pressure。如果中间件本身是资源消耗大(比如需要数据库连接),这个限制可能成问题——解决方法是在函数体内 .await 获取资源(但这会让 poll_ready 失去 back-pressure 语义)
二、分配开销:每个请求一次 Box::pin 和 BoxCloneSyncService::new——大概 100 ns。热路径场景(极低延迟网关)可能不想付这个代价
三、不能 generic over Service trait:middleware 函数签名里 Next 是具体类型,不能像 Tower Layer 那样被任意 Service 继承。这让 from_fn 中间件不能"到处贴"——只在 axum 的 Router 里用
四、FnMut 但不能捕获 &mut state:函数要 Clone + Send + 'static——不能捕获非 Clone 或非静态的东西。state 要通过 from_fn_with_state 传入
大多数业务场景这些限制无关紧要——但如果碰到,说明场景超出了 from_fn 的设计初衷,需要回到原生 Tower Layer。
from_fn 与 Tower 原生 Layer 的性能对比
| 维度 | from_fn | 原生 Tower Layer |
|---|---|---|
| 每请求额外分配 | 1 × Box::pin + 1 × BoxCloneSyncService::new | 0(如果 Service 写对) |
| vtable 调用 | 1 次(Next::inner.call 通过 trait object) | 0(编译期单态化) |
| 代码行数(简单 case) | ~10 行 | ~40 行 |
| 泛型参数数 | 1 个(T - 提取器 tuple) | 通常 ~3-5 个 |
| 调试信息 | 函数名可见 | 所有类型需要 Debug |
性能差异在微秒量级——对大多数业务可忽略。生产项目大部分中间件用 from_fn、性能关键的用原生 Layer——这种"90-10 分工"是合理工程选择。
中间件对 body 的处理
一个经常被忽略的细节:中间件能否修改 body 取决于使用方式。
场景一:不碰 body,纯透传 — 默认情况,最好:
rust
async fn mw(req: Request, next: Next) -> Response {
// req 按 FromRequest 的恒等 impl 拿到,不触发 body 消费
next.run(req).await
}场景二:读完 body 再继续 — 要消费再重构:
rust
async fn log_body(req: Request, next: Next) -> Result<Response, StatusCode> {
let (parts, body) = req.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024).await
.map_err(|_| StatusCode::PAYLOAD_TOO_LARGE)?;
tracing::info!(size = bytes.len(), "request body");
let req = Request::from_parts(parts, Body::from(bytes));
Ok(next.run(req).await)
}场景三:只想 peek 头几个字节 — 不读完整 body,但没有 stream peek 机制——只能读完 / 不读二选一。
第二种模式让 body 整个被缓冲到内存,不适合大 body。如果要做 body 相关处理(签名验证、内容检查),考虑:
- 限制 body 最大(
DefaultBodyLimit+ 中间件里的to_bytes(body, LIMIT)) - 文件上传等场景走特殊 route,没签名验证,或者签名验证 handler 自己做
from_fn 与 Handler 的类型关系
深入看会发现 from_fn 的 middleware 函数和 handler 在类型上几乎对称:
| 维度 | Handler | from_fn middleware |
|---|---|---|
| 签名形式 | async fn(提取器...) -> T: IntoResponse | async fn(提取器..., Next) -> T: IntoResponse |
| 提取器约束 | 前 N-1 个 FromRequestParts + 最后 FromRequest | 前 N-1 个 FromRequestParts + 最后 FromRequest + Next |
| 返回类型 | IntoResponse | IntoResponse |
| Error 契约 | Result<T, E: IntoResponse> | Result<T, E: IntoResponse> |
| 宏展开参数数 | 0-16 个提取器 | 0-15 个提取器(加 Next 共 16) |
| state 获取 | State<S> 从 .with_state 拿 | State<S> 从 from_fn_with_state 拿 |
区别只在末尾的 Next——其他维度几乎一模一样。这不是巧合——axum 团队有意让 middleware 函数的心智成本等于"handler + 一个 Next 参数",学会了 handler 就等于学会了 middleware。
从源码上看,FromFn::call 的实现和 HandlerService::call 的实现几乎平行——都是"提取参数 → 调用函数 → IntoResponse 转换"。差异只在 FromFn 多了一步"把 inner Service 包成 Next 塞给函数"。
这种设计一致性让 axum 的学习曲线相对平滑:不需要学两套不同的函数签名规则,一套"提取器 + async fn"的模型适用于 handler 和 middleware。
跨书关联:Tower 生态的 Layer 与 axum from_fn
Tower 原生 Layer 是整个 Rust 异步服务生态的通用抽象——用在 axum、tonic(gRPC)、数据库连接池、HTTP 客户端等。from_fn 是 axum 为 HTTP 中间件场景特制的"便利封装"——牺牲了通用性换来语法简洁。
两者关系:Layer 是基石、from_fn 是便利层。from_fn 本质上就是生成一个 Layer 实现——FromFnLayer + FromFn Service。写 from_fn 的中间件最终还是 Tower Layer——只是源码写起来不用手写 Layer/Service。
《Hyper 与 Tower:工业级 HTTP 栈》第 3 章详细讨论了 Layer + Service 的设计——包括为什么 Layer 是 "taking ownership of inner Service" 的模式、为什么 Service 的 poll_ready/call 分离。读那一章后再看 FromFn 的 Service impl,会发现它是最典型的 Tower Layer 模式——只是内部把逻辑委托给了用户的 async 函数。
axum 和 Tower Layer 的混用
from_fn 不等于全部——和 Tower 生态的 Layer 一起用是常态:
rust
use tower_http::{trace::TraceLayer, timeout::TimeoutLayer, cors::CorsLayer};
let app = Router::new()
.route("/", get(handler))
.route("/protected", get(protected))
.route_layer(middleware::from_fn_with_state(state.clone(), auth)) // 自定义 from_fn
.layer(TraceLayer::new_for_http()) // Tower Layer
.layer(CorsLayer::permissive()) // Tower Layer
.layer(TimeoutLayer::new(Duration::from_secs(30))) // Tower Layer
.layer(HandleErrorLayer::new(timeout_handler)) // axum 适配
.with_state(state);执行顺序还是遵循洋葱——越靠后 layer 的越外层、先看到请求。Tower Layer 和 from_fn 没有本质区别——它们在 Layer 栈里一视同仁。差别只在代码怎么写。
何时选 Tower Layer、何时选 from_fn
| 需求 | Tower Layer | from_fn |
|---|---|---|
| 生态标准方案已存在(compression、tracing、cors、timeout) | ✓ 直接用 | 自己写 from_fn 重复造轮子 |
| 业务特定的认证 / rate limit / 计费 | 复杂 | ✓ 自然写 |
| 希望发布给其他 axum / tonic / 通用 tower 项目用 | ✓ 通用 | 只能 axum |
希望 poll_ready 真正做 back-pressure | ✓ | 不支持 |
| 快速原型 | 繁琐 | ✓ |
实际项目里 tower-http 的几个中间件几乎是标配——TraceLayer 做日志、CorsLayer 做 CORS、TimeoutLayer 做超时、CompressionLayer 做压缩。自己的业务逻辑(认证、配额、计费)写 from_fn。两者混用让项目既享受生态又保留灵活性。
本章总结
from_fn 是 axum 最常用的中间件工具。核心要点:
一、签名和 handler 对称:学了 handler 就会 from_fn——只多一个 Next 参数
二、Next 是剩余栈的抽象:用 BoxCloneSyncService 类型擦除,让 middleware 函数签名固定。next.run(req).await 透传、不调则短路
三、错误模型统一:Result<Response, E: IntoResponse> + ?、Error 是 Infallible——和 handler 一模一样
四、state 通过 from_fn_with_state 注入:不能直接捕获 Router state——这个限制让 from_fn 和 Router 的 state 保持类型解耦
五、性能代价可接受:每请求多 100 ns 的 box 分配——millisecond 级业务完全忽略
六、不是唯一选择:生态标准方案用 tower-http、业务特定用 from_fn——混用是常态
更深的几个设计原则也值得记住:
一、"洋葱模型"是自然的:Tower 的 Layer 叠加 + from_fn 的 next.run 前后代码,自然形成 before/after 对称的洋葱——不需要框架特殊配置
二、类型驱动的 middleware 栈:签名里 State<S>、提取器、Next 都是类型级约束——编译期就能保证 middleware 栈自洽
三、错误路径永远回归 Response:从 handler 到 middleware 到框架,所有 Err 都必须能 IntoResponse——这条"全链路 Infallible"让生产稳定性有 foundation
下一章继续讲另外三种中间件 helper——map_request / map_response / from_extractor。它们是 from_fn 和原生 Layer 之间的中间形态——适合"只要改 request"、"只要改 response"、"只要在 handler 前做提取器校验"这三种更窄的场景。每种都有自己的 API 简化点。理解了 from_fn 之后,下一章的几个 helper 会感觉像是"针对特定场景的进一步简化"——核心心智模型不变。