Appearance
第6章 FromRequest 与 FromRequestParts:提取器的所有权分裂
一条物理约束
第 5 章里 impl_handler! 宏展开后有一个分裂:前 N-1 个参数用 FromRequestParts,最后一个参数用 FromRequest。当时我们只给出了结论——"请求体只能被消费一次,所以只有最后一个参数可以消费 body"。这一章要把这条约束放到显微镜下。
HTTP 请求的 body 在 Rust 里用 http_body::Body trait 表示。它的 poll_frame 方法按需从网络或内存异步拉取数据帧——每一帧数据被 poll 出来后,内部状态指针向前推进,已消费的帧不会再回来。这不是 Axum 的设计选择,而是 hyper 和 HTTP 协议本身的特性:客户端把字节流推过来,服务端一次性读走,要么处理要么丢弃。想"回放"body,只能先把它完整缓冲到 Vec<u8> 再从头开始——这正是 Bytes::from_request 干的事情,但它也消费了原始 body。
这条物理约束投射到类型系统上,就是两个 trait 的分裂:
- 一个操作只借用
&mut Parts(头部、方法、URI、extensions),可以被多个提取器按顺序调用,每个读各自关心的字段,不互相干扰; - 一个操作消费整个 Request(含 body),一旦调用就拿走了请求的所有权,后面的提取器再也拿不到 body。
前者是 FromRequestParts,后者是 FromRequest。Axum 用 Rust 类型系统把"最多一次 body 消费"这条运行时约束编译化——handler 参数的编译顺序即是提取的执行顺序,编译器直接拒绝"有两个参数都要消费 body"的写法,完全没有运行时检查的必要。
body 在协议层为什么是一次性的
"Body 只能读一次"这条约束看起来像 Rust 所有权的设计选择,但它实际从 HTTP 协议层就已经确定了。梳理一下这条链的物理基础有助于理解 Axum 为什么不能"假装能回放 body"。
HTTP/1.1 在 RFC 7230 里规定 body 的传输有两种模式:Content-Length: N 表示固定长度、接下来 N 个字节就是 body;Transfer-Encoding: chunked 表示分块、每块先发长度再发数据,最后一个零长度块表示结束。无论哪种,字节一旦从 TCP 套接字流出到服务端的读缓冲区,网络层就没有"退货"机制——TCP 的序号号段前进了,客户端不会重发相同字节。服务端唯一能做的是把这些字节先拷到内存里。
HTTP/2 / HTTP/3 在帧层面类似:body 以 DATA 帧的形式分多次抵达,每帧消费后 flow control window 前进,服务端如果想再读一遍必须自己保留副本。
hyper 在 Rust 里把这些协议细节统一抽象成 http_body::Body trait 的 poll_frame(Pin<&mut Self>, &mut Context<'_>) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>>——按需拉取下一帧,拉到 None 为止。poll_frame 的签名里隐含着"不可 seek、不可 peek"的语义:只有 &mut self、只有前进、没有后退。
Axum 的 Body 类型(axum_core::body::Body)是 http_body::Body 的一个具体实现,内部包装了动态分发的 body 流。它继承了同样的语义:消费一帧就丢一帧。如果想让 body 被复读,唯一办法是先调 Bytes::from_request 把全部字节缓冲到 Vec<u8>——但缓冲本身也消费了 body。
这条物理约束一路延伸到 Rust 类型系统:
Rust 的所有权规则不是"附加"在物理约束上的:它是物理约束的编译期映射。Request 按值传入 from_request(req: Request, ...),编译器把 req 的所有权 move 进去,函数外 req 就不能再用——这恰好对应"网络上字节流前进了、拿不回来了"的现实。如果 Axum 选择 from_request(req: &mut Request),编译器会允许多个 FromRequest 轮流拿 &mut、各自消费 body,类型系统就不再反映物理约束了,Bug 会延迟到运行时。
FromRequestParts:借用 &mut Parts
trait 定义在 axum-core/src/extract/mod.rs:53-63:
rust
// axum-core/src/extract/mod.rs:53-63
#[diagnostic::on_unimplemented(
note = "Function argument is not a valid axum extractor. \
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details"
)]
pub trait FromRequestParts<S>: Sized {
type Rejection: IntoResponse;
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}trait 的三个设计决策值得逐一拆解。
&mut Parts 而不是 &Parts:虽然多数提取器只读头部字段(Method、Uri、HeaderMap),但某些提取器需要写 parts.extensions——比如路径参数的匹配结果会被 Router 提前塞进 parts.extensions,Path<T> 提取器从 extensions 里取出并反序列化;State<S> 的内部实现也涉及 extensions 的读取与 clone。&mut 同时让一个链条上的多个提取器可以"前一个往 extensions 里放东西,后一个取出",形成隐式的流水线通信通道。
Sized bound:FromRequestParts<S>: Sized 和 handler 的 trait bound 呼应。提取出的类型必须是确定大小,才能在栈上接收——handler 的签名 async fn handler(m: Method, u: Uri) 要求 Method 和 Uri 都可在栈上分配。
impl Future<Output = …> + Send 而不是 async fn:两者几乎等价,但 impl Future + Send 显式要求返回的 Future 是 Send——这是 handler 能在多线程运行时调度的必要条件。Rust 的 async fn 在 trait 里返回的 Future 默认不 Send(因为它可能捕获非 Send 的 State),手写 impl Future + Send 强制 bound。
Rejection 与 IntoResponse 的硬契约
type Rejection: IntoResponse 是这个 trait 最重要的约束:提取失败时返回的错误必须能转成 HTTP 响应。这不是可选的——是 trait 定义的一部分。
为什么是这样?回到第 5 章讲的 Handler 的 Future<Output = Response> 保证:handler 不输出错误,所有错误在 handler 内部已转成 Response。这个保证往下推,就要求每个提取器的 Rejection 也能立即变 Response。rejection.into_response() 在 impl_handler! 的短路返回处被直接调用(第 5 章 line 203),没有中间错误类型的映射环节。
这个契约的一个副作用:如果你想写一个"内部错误类型不是 HTTP 响应概念"的提取器——比如业务错误码、数据库异常——你必须自己先 impl IntoResponse for 那个错误类型。这强制了一种设计纪律:提取失败是 HTTP 层的事情(通常是 4xx),不要把业务错误混进提取阶段。提取只决定"请求对不对",不决定"业务能不能做"。
&mut Parts 上的流水线:借用检查的妙用
第 5 章 impl_handler! 宏展开里有这段:
rust
let T1 = T1::from_request_parts(&mut parts, &state).await?;
let T2 = T2::from_request_parts(&mut parts, &state).await?;两行连续调用,每行都独立地拿 &mut parts。初看会让人担心借用冲突:&mut 应该是排他的,怎么能连续两次?
答案在 Rust 借用检查器的时序逻辑。第一行调用 T1::from_request_parts 时 &mut parts 从 parts 借出,但 .await 之后函数返回,&mut 借用随即归还——借用是作用域内存在的,函数一返回就结束。第二行重新从 parts 借一个新的 &mut 给 T2::from_request_parts。两次借用时间窗口不重叠,borrow checker 放行。
这是 &mut 比 & 微妙的地方:跨 await 点持有 &mut 确实有一些问题(Future 会捕获借用,变得非 Send 等),但跨 await 前后轮流借用完全合法。from_request_parts 签名 (parts: &mut Parts, state: &S) -> impl Future<Output=...> 把 &mut parts 借用绑定到"调用"而非"调用结果 Future 的生命周期"——Future 本身不持有 &mut parts,而是在 .poll() 完成前就把借用用完了。
这条设计让宏展开出的代码在类型层面完全编译通过、运行时语义零开销。如果签名写成 (parts: &'a mut Parts, state: &'a S) -> impl Future<Output=..> + 'a,借用会延长到 Future 销毁——两次连续调用就需要第一个 Future 先 drop、再开始第二个,多出一层生命周期约束。Axum 选择了前者的"函数内部用完借用"的形态,让宏展开的代码保持朴素。
你可以把 &mut Parts 想成一条共享数据管道:
每次借用窗口短、不跨 await 持有,所有提取器依次在同一条 parts 流水线上工作,彼此通过 extensions 隐式通信。
FromRequest:消费 Request
axum-core/src/extract/mod.rs:79-89:
rust
// axum-core/src/extract/mod.rs:79-89
#[diagnostic::on_unimplemented(
note = "Function argument is not a valid axum extractor. \
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details"
)]
pub trait FromRequest<S, M = private::ViaRequest>: Sized {
type Rejection: IntoResponse;
fn from_request(
req: Request,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}和 FromRequestParts 的差异只有两处:from_request 的第一个参数是 Request(按值、含 body)而不是 &mut Parts;trait 多了一个泛型参数 M,默认值 private::ViaRequest。前一个是本质差异,后一个是机制——M 是 Axum 最精巧的类型系统小把戏之一。
M 参数:ViaParts 与 ViaRequest
mod.rs:31-37 定义了两个空 enum:
rust
// axum-core/src/extract/mod.rs:31-37
mod private {
#[derive(Debug, Clone, Copy)]
pub enum ViaParts {}
#[derive(Debug, Clone, Copy)]
pub enum ViaRequest {}
}两个 enum 都没有构造器(无 variant),永远不可能有值——它们是纯粹的类型级标记。FromRequest<S, M> 的 M 参数就是从这两个里选一个:
M = ViaRequest:这是原生FromRequest实现,比如Json<T>——impl FromRequest<S> for Json<T>省略了M就是默认的ViaRequest。M = ViaParts:这是派生FromRequest实现,通过 blanket impl 从FromRequestParts自动来。
为什么需要两个不同的 M?因为 Axum 要做一件事:所有 FromRequestParts<S> 的类型都自动是 FromRequest<S, ???>。如果不区分 M,这个 blanket impl 就会和"原生"的 impl FromRequest<S> for Json<T> 冲突——Rust 的一致性规则会拒绝:"编译器不知道 Json<T> 是不是也实现了 FromRequestParts<S>,所以两条 impl 可能都适用,你在撒谎"。M = ViaParts vs M = ViaRequest 用不同的类型参数把两条 impl 分到不同命名空间,消除冲突。
当 handler 的最后一个参数是 Json<T>,编译器找 impl Handler<(ViaRequest, T1, T2, Json<T>), S>——M 推导为 ViaRequest;当最后一个参数是 Path<u32>,编译器找 impl Handler<(ViaParts, T1, T2, Path<u32>), S>——M 推导为 ViaParts。两条路径在类型层彻底分开,不会重叠。
用户完全感受不到 M 的存在——post(create_user) 的推导完全自动。但这个"看不见的泛型参数"恰恰是让 Handler trait 能同时容纳两种 impl 风格的关键。
blanket impl:FromRequestParts ⇒ FromRequest<ViaParts>
mod.rs:91-105 完成了这个"自动派生":
rust
// axum-core/src/extract/mod.rs:91-105
impl<S, T> FromRequest<S, private::ViaParts> for T
where
S: Send + Sync,
T: FromRequestParts<S>,
{
type Rejection = <Self as FromRequestParts<S>>::Rejection;
fn from_request(
req: Request,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> {
let (mut parts, _) = req.into_parts();
async move { Self::from_request_parts(&mut parts, state).await }
}
}核心是 let (mut parts, _) = req.into_parts();——把 body 丢弃(_ 接住),只用 parts 走 from_request_parts。Rejection 类型也从 FromRequestParts::Rejection 直接继承。这个 impl 让"一个只读取头部字段的提取器"可以坐在 handler 参数列表的任何位置,包括最后——因为最后位置要求 FromRequest,而 blanket impl 已经把它"免费升级"成了 FromRequest<S, ViaParts>。
第 5 章里 impl_handler! 宏的 $last: FromRequest<S, M> + Send 这行 bound 现在就清楚了:当 $last 是 Path<u32>(FromRequestParts)时,M 被推导为 ViaParts;当 $last 是 Json<T>(原生 FromRequest)时,M 被推导为 ViaRequest。同一个宏展开同时支持两种情况,没有额外的代码路径。
Result<T, E>:用 Infallible 把拒绝变成值
mod.rs:107-129 里有两条看似简单但意义重大的 impl:
rust
// axum-core/src/extract/mod.rs:107-117
impl<S, T> FromRequestParts<S> for Result<T, T::Rejection>
where
T: FromRequestParts<S>,
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Ok(T::from_request_parts(parts, state).await)
}
}
// axum-core/src/extract/mod.rs:119-129(类似,for FromRequest)这意味着:如果一个 handler 参数写成 result: Result<Path<u32>, PathRejection>,Path 的提取失败不会短路整个 handler——而是把 Err(rejection) 原封不动作为参数传给 handler,handler 自己决定怎么处理。
这个机制的价值在"提取失败不一定是致命错误"的场景里:
- 某个头部可选:
result: Result<TypedHeader<Authorization>, _>——有就用,没有就走匿名分支 - 多路径对齐:一个接口兼容多种认证方式,哪种都可能失败,失败不立即 4xx
- 降级策略:JSON 解析失败降级成查询参数再尝试
Rejection = Infallible 是关键——因为 Result<T, E> 的提取永远不会失败(失败变成了 Err 值本身),所以告诉编译器"这个提取器的 Rejection 类型不可能实例化"。handler 收到这个参数时,编译器已经知道短路分支不会触发,生成的机器码里短路 match 被优化掉。这和第 5 章讲的 Service::Error = Infallible 是同一种技巧——用 Infallible 类型告诉编译器"这条路径不存在"。
Result<T, E> 的提取器是一个容易忽视但极其有用的通用模式。它把"拒绝响应"从控制流层面(短路 return)降级到数据流层面(包在 Err 里传递),让 handler 拿回了完整决策权。
错误信息:#[diagnostic::on_unimplemented]
两个 trait 定义上方都有 #[diagnostic::on_unimplemented(note = "...")]。这是 Rust 1.78 稳定的属性(RFC 2397),让 trait 作者给"这个 trait 没实现"的错误消息添加补充说明。
普通情况下,用户把一个非提取器类型塞进 handler 参数(比如写 pool: PgPool 忘了 State 包装)时,编译器报的是一整片 Handler 的候选 impl。on_unimplemented 的 note 会在底部追加一行:
text
note: Function argument is not a valid axum extractor.
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details这条 note 不解决定位问题(定位问题要靠第 5 章介绍的 #[debug_handler]),但它指明了方向——读者至少知道"这是个 extractor 相关的问题,去看 extract 模块的文档",而不是在 Handler trait 候选列表里迷路。
axum-core 的 #[diagnostic::do_not_recommend](tuple.rs:6、option.rs:40 等)是配套属性——它告诉编译器"报错时不要建议这个 impl 作为修复方向"。元组提取器的 impl 是"所有元组都自动实现"的 blanket impl,它几乎总不是用户想要的修复方向;do_not_recommend 让编译器在建议中跳过它,错误信息更聚焦。
这类 diagnostic::* 属性都是"不改变类型系统行为、只改变错误消息"的纯用户体验工程。Axum 用得比绝大多数 Rust 库都积极——这是它对错误信息体验的重视。第 19 章讲 axum-macros 时,我们会看到 #[debug_handler] 是如何把这一系列属性编排起来生成更高层的错误诊断。
Option<T>:有就用,没有就跳过
mod.rs 在顶部 pub use self::option::{OptionalFromRequest, OptionalFromRequestParts}; 导出两个"可选版"trait。定义在 axum-core/src/extract/option.rs:11-22:
rust
// axum-core/src/extract/option.rs:11-22
pub trait OptionalFromRequestParts<S>: Sized {
type Rejection: IntoResponse;
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Option<Self>, Self::Rejection>> + Send;
}注意返回的是 Result<Option<Self>, Rejection>——有三种结果:
Ok(Some(value)):字段存在且提取成功Ok(None):字段不存在(视为合法情况)Err(rejection):字段存在但提取失败(视为错误)
这和 Result<T, E> 提取器的语义不同。Result<Path<u32>, _> 里"Path 格式错误"和"Path 不存在"都是 Err;Option<X> 里"存在但格式错"还是会 short-circuit 报错,只有"不存在"才变成 None。两者各有适用场景。
option.rs:40-55 的 blanket impl 把 OptionalFromRequestParts 升级成 FromRequestParts:
rust
#[diagnostic::do_not_recommend]
impl<S, T> FromRequestParts<S> for Option<T>
where
T: OptionalFromRequestParts<S>,
S: Send + Sync,
{
type Rejection = T::Rejection;
// ... 直接转发 ...
}这样 handler 参数可以直接写 maybe_token: Option<TypedHeader<Authorization<Bearer>>>——只要 TypedHeader<...> 实现了 OptionalFromRequestParts,编译器就会自动接受 Option<TypedHeader<...>>。
#[diagnostic::do_not_recommend] 在这里同样重要:它告诉编译器"错误恢复建议时不要推荐包 Option"——否则编译器在 pool: PgPool 忘 State 包装的场景下可能建议"试试 Option<PgPool> 吧",完全没帮助。
对比三种"允许失败"的提取器模式:
| 写法 | 字段不存在 | 存在但格式错 | Rejection 语义 |
|---|---|---|---|
T(直接提取) | 短路 → 拒绝响应 | 短路 → 拒绝响应 | 任何失败都立即返回 4xx |
Result<T, T::Rejection> | Err(...) | Err(...) | 失败变成数据,handler 决策 |
Option<T> | None | 短路 → 拒绝响应 | 只"不存在"降级,格式错仍报错 |
设计表情下背后的直觉:Option<T> 表达"字段是可选的,但一旦出现就必须合法";Result<T, E> 表达"提取随时可能失败,handler 来判断后续怎么走"。选择哪个,看业务语义。
元组提取器:把多种 Rejection 统一到 Response
axum-core/src/extract/tuple.rs 为所有 1-16 元素元组实现了 FromRequestParts 和 FromRequest。先看单元类型 () 的 impl(tuple.rs:7-16):
rust
// axum-core/src/extract/tuple.rs:7-16
#[diagnostic::do_not_recommend]
impl<S> FromRequestParts<S> for ()
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(_: &mut Parts, _: &S) -> Result<(), Self::Rejection> {
Ok(())
}
}空元组 () 的提取平凡且永不失败——Rejection 是 Infallible。然后 impl_from_request! 宏(tuple.rs:18-75)为 (T1,)、(T1, T2,) ... (T1, ..., T16,) 分别生成 impl,遵循第 5 章 handler 的那套模式:前 N-1 个元素用 FromRequestParts、最后一个用 FromRequest。关键的一行:
rust
// axum-core/src/extract/tuple.rs:30(简化)
type Rejection = Response;所有元组的 Rejection 都是统一的 Response。为什么不保持每个元素自己的 Rejection?因为元组各元素的 Rejection 类型不一样——(Path<u32>, Json<T>) 里 Path 和 Json 失败是不同类型。元组自己不能既是 PathRejection 又是 JsonRejection,只能抹平成一个共同上界——IntoResponse 的最顶端就是 Response 本身(Response: IntoResponse 是恒等 impl)。
实现里的 map_err(|err| err.into_response())?(tuple.rs:36)完成这个抹平:每个元素的 Rejection 先被 .into_response() 转成 Response,然后用 ? 短路返回。元组 impl 等于"把每个元素的提取结果 try-question-mark 起来,错误统一成 Response"。
这让元组可以作为 handler 参数:
rust
async fn handler((method, uri): (Method, Uri)) { /* ... */ }元组作为参数只占一个"槽位",但内部展开 2 个值。结合第 5 章讲的 M 参数,这里 (Method, Uri) 会走 FromRequestParts 路径,因为 Method 和 Uri 都是 FromRequestParts。测试用例(tuple.rs:94-118)里的 assert_from_request_parts::<(((Method,),),)>(); 展示了元组可以任意嵌套——编译期递归展开。
FromRef:State 的子状态抽取
from_ref.rs:14-26 定义了一个小但高度通用的 trait:
rust
// axum-core/src/extract/from_ref.rs:14-26
pub trait FromRef<T> {
fn from_ref(input: &T) -> Self;
}
impl<T> FromRef<T> for T
where
T: Clone,
{
fn from_ref(input: &T) -> Self {
input.clone()
}
}FromRef<T> 是"从 &T 产出 Self"的抽象。blanket impl 说:任何 Clone 类型都自动 FromRef 自己(通过 .clone())。这看起来平平无奇,但在 State 提取器和子状态设计中是枢纽——第 18 章会详细讨论。这里先预告:当应用状态是一个大结构体 AppState { db: PgPool, redis: RedisClient, config: Arc<Config> },而某个 handler 只需要 db 时,可以让 PgPool: FromRef<AppState>(通过 #[derive(FromRef)]),然后 handler 参数写 State(db): State<PgPool>——State 内部用 FromRef 从主状态抽出子状态。
第 18 章的 State 管理会用 mermaid 画出整个抽取链条;这里记住一点:FromRef 是 axum-core 定义的,不是 axum 定义的——它是提取器框架的基础抽象之一,和 FromRequestParts、FromRequest 平级。
一个完整的 FromRef 例子
假设应用全局状态是这样:
rust
#[derive(Clone)]
struct AppState {
db: PgPool,
redis: RedisClient,
config: Arc<Config>,
}第一个 handler 需要所有三个字段,第二个只需要 db,第三个只需要 config。最朴素写法是每个 handler 都提取整个 State<AppState> 然后从字段里取——能工作但每个 handler 的签名都和 AppState 整体绑死,AppState 长出新字段时签名不变但编译器认为"依赖 AppState 的字段变了"可能需要重新 link。
更好的写法是让 PgPool: FromRef<AppState>、Arc<Config>: FromRef<AppState>。手写 impl:
rust
impl FromRef<AppState> for PgPool {
fn from_ref(input: &AppState) -> Self { input.db.clone() }
}
impl FromRef<AppState> for Arc<Config> {
fn from_ref(input: &AppState) -> Self { input.config.clone() }
}或者更简洁,用 axum-macros 的 derive:
rust
#[derive(Clone, FromRef)]
struct AppState {
db: PgPool,
redis: RedisClient,
config: Arc<Config>,
}#[derive(FromRef)] 会为每个字段自动生成 FromRef<AppState> for FieldType 的 impl——前提是字段类型是 Clone 的(因为从 &AppState 产出 FieldType 需要 .clone())。
于是 handler 可以按需提取:
rust
async fn get_user(State(db): State<PgPool>) -> impl IntoResponse { /* ... */ }
async fn reload_config(State(cfg): State<Arc<Config>>) -> impl IntoResponse { /* ... */ }
async fn full(State(state): State<AppState>) -> impl IntoResponse { /* ... */ }三个 handler 挂在同一个 Router<AppState> 上。State<T> 的内部实现(第 18 章详讲)会调用 T::from_ref(&app_state) 得到对应子状态——本质上是"从 main state clone 出一个子字段"。
这个设计的价值:
- 签名清晰:handler 的参数类型直接标明它需要什么,不需要读 handler 体才知道它用了哪些字段
- 重构友好:给
AppState加字段不影响已有 handler;删除一个已有字段会让依赖它的 handler 编译失败,提供了依赖可视化 - 测试简化:测试
reload_config时可以构造一个AppState { config, db: 空 pool, redis: 假 client },或者更进一步,只实现FromRef<TestState> for Arc<Config>连AppState都不用
代价:每次提取都 .clone()。对 Arc<T> / PgPool(内部是 Arc<Pool>)来说是零成本原子加一;对大结构体按值拷贝就是灾难——所以 FromRef<AppState> 的 impl 要么返回 Arc / 连接池这类廉价 clone 类型,要么不要让大结构体作为子 state。这条工程直觉在第 18 章有详细讨论。
FromRef 的另一个应用是"多 Router 合并时的 state 统一"——当两个子 Router 各自要求不同 state,用 FromRef 让它们都从主 state 派生,合并 Router 时类型对齐。第 4 章的 Router::merge 在这一点上是强约束,编译器要求合并前两个 Router 的 state 类型完全一致,FromRef 是达到这个一致性的主要手段。
请求本身作为提取器:Request 是 FromRequest
axum-core/src/extract/request_parts.rs:8-17:
rust
// axum-core/src/extract/request_parts.rs:8-17
impl<S> FromRequest<S> for Request
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
Ok(req)
}
}Request 自己实现了 FromRequest——提取即是恒等。这让 handler 可以写:
rust
async fn raw(req: Request) -> impl IntoResponse { /* 自己处理一切 */ }这是"跳出自动提取、手动处理请求"的入口——你拿到整个 Request,爱怎么用怎么用。结合 Method、Uri、HeaderMap(request_parts.rs:19-68 都是 FromRequestParts 的简单 clone impl),handler 可以在"全自动提取"和"完全手动"之间任意拼装。
但有个隐含约束:Request 是 FromRequest(消费 body),所以它只能是最后一个参数。async fn handler(req: Request, method: Method) 会编译失败——因为 Method 在 req 之后,但 req 已经把整个请求拿走了。正确写法是 async fn handler(method: Method, req: Request)。这条规则在第 5 章的宏展开里已经编码进了类型系统,这里只是具体化一次。
实战:手写一个 TypedUser 提取器
理论说到这,做一个完整的例子把所有机制串起来。需求:handler 想要一个 CurrentUser 参数,从 Authorization: Bearer <token> 头里解析出来。场景:
rust
async fn get_profile(user: CurrentUser) -> impl IntoResponse {
Json(user)
}CurrentUser 不是 Axum 内置类型,需要自己实现提取器。走完整流程。
第一步:决定是 FromRequestParts 还是 FromRequest。这里只读 Authorization 头——不碰 body。所以实现 FromRequestParts<S>。这让 CurrentUser 能出现在 handler 参数的任何位置,包括和 Json<T> 共存。
第二步:定义 Rejection 类型。用户的认证可能有三种失败模式:头不存在、格式不对、token 解码失败。三个 variant 的 enum,手写 IntoResponse:
rust
use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
#[derive(Debug)]
pub enum AuthRejection {
MissingAuthorization,
InvalidFormat,
InvalidToken(String),
}
impl IntoResponse for AuthRejection {
fn into_response(self) -> Response {
let (status, body) = match self {
Self::MissingAuthorization => (StatusCode::UNAUTHORIZED, "missing Authorization header"),
Self::InvalidFormat => (StatusCode::BAD_REQUEST, "bad Authorization format"),
Self::InvalidToken(_) => (StatusCode::UNAUTHORIZED, "token verification failed"),
};
(status, body).into_response()
}
}注意每种失败都映射到合适的 HTTP 状态码:缺失或无效的 token 是 401(身份问题),格式错是 400(请求本身不合法)。这种 HTTP 语义的精细化映射是提取器设计的一部分——不同失败应该让客户端看到不同的行为。
第三步:实现 FromRequestParts:
rust
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
#[derive(Debug, serde::Serialize)]
pub struct CurrentUser {
pub id: u64,
pub name: String,
}
impl<S: Send + Sync> FromRequestParts<S> for CurrentUser {
type Rejection = AuthRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let header = parts
.headers
.get("Authorization")
.ok_or(AuthRejection::MissingAuthorization)?;
let raw = header.to_str().map_err(|_| AuthRejection::InvalidFormat)?;
let token = raw.strip_prefix("Bearer ").ok_or(AuthRejection::InvalidFormat)?;
// 用任意 JWT 库解码——这里伪代码
let claims = decode_jwt(token).map_err(|e| AuthRejection::InvalidToken(e.to_string()))?;
Ok(CurrentUser {
id: claims.user_id,
name: claims.name,
})
}
}几个值得注意的点:
<S: Send + Sync>是泛型——CurrentUser对任何状态类型都能提取,不依赖具体S。这意味着CurrentUser在使用了AppState的 Router 里和使用了()的 Router 里都能工作。如果需要从 state 里拿 JWT 公钥等配置,可以收窄S为具体类型——下面一个小变体:parts.headers是HeaderMap,.get("Authorization")返回Option<&HeaderValue>,通过.ok_or(...)转成Resultheader.to_str()因为HeaderValue可能含非 ASCII 字节,显式转&str时可能失败- 通过
?层层短路,最后返回Ok(CurrentUser { ... })
第四步:如果需要 state,换成:
rust
impl FromRequestParts<AppState> for CurrentUser {
type Rejection = AuthRejection;
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
let token = /* ... 同上 ... */;
let claims = decode_jwt(token, &state.jwt_public_key)
.map_err(|e| AuthRejection::InvalidToken(e.to_string()))?;
Ok(CurrentUser { id: claims.user_id, name: claims.name })
}
}当 S 收窄为具体类型后,CurrentUser 只能用在 Router<AppState> 里。好处是可以访问 state 上的配置——公钥、数据库连接、RPC 客户端都能取。
第五步:使用:
rust
async fn get_profile(user: CurrentUser) -> impl IntoResponse {
Json(user)
}
// 或者和其他提取器共存
async fn update_profile(
user: CurrentUser,
Path(id): Path<u64>,
Json(payload): Json<UpdatePayload>,
) -> impl IntoResponse {
if user.id != id { return StatusCode::FORBIDDEN.into_response(); }
/* ... */
}CurrentUser 和 Path、Json 共存完全自然——因为它实现了 FromRequestParts,可以放在任何位置。编译器通过第 5 章讨论的宏推导 T 参数,认识到"这是个 parts-only 提取器,放最后一个之前的任何位置都可以"。
这个例子完整体现了 axum 提取器的所有关键约束:
| 约束 | 本例如何体现 |
|---|---|
| trait 选择 | 只读 headers → FromRequestParts(不是 FromRequest) |
| Rejection | 自定义 enum 实现 IntoResponse,映射到合适的 HTTP 状态码 |
| 提取逻辑 | 从 parts.headers 读、各种失败模式用 ? 短路 |
| state 使用 | 可以用 S: Send + Sync 泛型,也可以收窄为具体类型 |
| 和其他提取器共存 | parts-only 能放任何位置 |
#[derive(FromRequest)](来自 axum-macros)可以把这类模板化的代码用宏生成——给一个结构体 struct UserRequest { user: CurrentUser, body: Json<Payload> },derive 宏把字段逐个调用内层提取器并填入。我们在第 19 章会看到这类宏如何通过 T 参数和 M 推导生成正确的 bound。目前手写一次足以理解整套机制。
DefaultBodyLimit:extensions 通道的典型用法
default_body_limit.rs 展示了 extensions 作为提取器间通信通道的经典用法。DefaultBodyLimit 是一个 tower::Layer,layer 后返回的 Service 在 call 时做一件事(default_body_limit.rs:223-227):
rust
// axum-core/src/extract/default_body_limit.rs:223-227
fn call(&mut self, mut req: Request<B>) -> Self::Future {
req.extensions_mut().insert(self.kind);
self.inner.call(req)
}把 DefaultBodyLimitKind(Disable 或 Limit(usize))塞进 req.extensions。后续的 Bytes::from_request、String::from_request、Json::from_request 等 body 消费型提取器读这个 extension,决定是否限制 body 大小。默认限制是 2 MB(DefaultBodyLimit 文档:default_body_limit.rs:7-8)。
这个模式是 Axum 里 Service layer 和 Extractor 协作的标准范式:
Layer 不修改 trait 契约,也不改变 handler 的 API——它只是"往 extensions 里埋一点上下文"。下游的提取器决定怎么解释。这种"低耦合上下文传递"让不同中间件和提取器能够组合使用而不互相感知——第 13 章讲中间件时会看到更多这种模式。
Rejection 类型:组合与定义的宏
axum-core/src/extract/rejection.rs 展示了 axum 内部如何用宏定义一组互相组合的 rejection 类型。两个核心宏:
define_rejection!:定义一个叶子节点 rejection(带 HTTP 状态码、响应体、关联的 error)composite_rejection!:把若干叶子节点组合成一个 enum,自动生成From<Leaf> for Composite和IntoResponse for Composite的 impl
举例,rejection.rs:40-48 定义 LengthLimitError:
rust
// axum-core/src/extract/rejection.rs:40-48
define_rejection! {
#[status = PAYLOAD_TOO_LARGE]
#[body = "Failed to buffer the request body"]
/// Encountered some other error when buffering the body.
pub struct LengthLimitError(Error);
}简短的 DSL 生成了一个完整的 rejection struct:pub struct LengthLimitError(Error)、impl IntoResponse for LengthLimitError(返回 413 状态码 + 给定 body)、impl From<BoxError> for LengthLimitError,以及符合 axum_core::Error 的 Debug/Display。
然后 rejection.rs:65-73 组合:
rust
composite_rejection! {
pub enum BytesRejection {
FailedToBufferBody,
}
}生成一个 enum,每个 variant 自动 From 对应的叶子类型,并且 impl IntoResponse 通过 match 分发到各 variant 的 response。
这套宏是 axum-core 的内部实现细节,用户不直接调用——但用户自己实现提取器时,可以参考这个模式手写 rejection 类型。define_rejection! / composite_rejection! 都是 #[macro_export] 过 __define_rejection / __composite_rejection 前缀导出的(rejection.rs:3-4),第三方 crate 也可以复用。
设计回响:为什么不是"一个统一的 Extractor trait"?
到这里我们可以回答一个本来应该在开场问的问题:为什么是两个 trait 而不是一个?Axum 完全可以设计一个统一的 trait Extractor<S> { fn extract(req: &mut Request, state: &S) -> ...; },让提取器自己在 &mut Request 上操作。但实际没有。原因有三层:
第一层,类型系统诚实表达物理约束。&mut Request 允许每个提取器"碰到"整个请求,包括 body——但物理上 body 只能被消费一次。Axum 用两个 trait 把"只借用 Parts"和"消费 Body"在类型层切分清楚:你想消费 body 就实现 FromRequest,否则只能实现 FromRequestParts。编译器直接在 impl 声明处强制这个约束,不是运行时 panic、不是文档约定,而是编译错误。
第二层,顺序语义。第 5 章的 impl_handler! 宏展开里,FromRequestParts 的调用发生在前,FromRequest 的调用发生在最后且只能有一个。如果只有一个 trait,"前 N-1 个和最后一个的区别"就无法在 trait 层面表达,impl_handler! 宏需要运行时检查"哪个参数消费了 body",而这在编译期做不到。两个 trait 让第 5 章那一整套机制自然落地。
第三层,Blanket impl 的方向性。FromRequestParts ⇒ FromRequest 是单向的(忽略 body 即可);反过来不成立(一个 FromRequest 可能必须消费 body,没法只看 Parts)。两个 trait 用这种不对称关系表达"parts-only 能力是 body 能力的子集"。如果合成一个 trait,这种子集关系要靠运行时探测或类型层奇技淫巧(negative bound、specialization)来表达,而 Rust 当前都不稳定。
这个设计选择回应了 Rust 类型系统的一个深层风格:不把运行时语义隐藏到 trait 默认方法里。如果 extract 可以同时处理两种情形(借用 Parts / 消费 Body),trait 定义就得有两个方法,调用方得知道什么时候调哪个——从 API 洁癖角度看是劣于拆成两个 trait 的。Axum 选择了后者,付出的代价是有一个 M 参数和一个 blanket impl,收益是 handler 参数规则完全类型化。
与 Serde Deserializer 的对比
FromRequestParts / FromRequest 的形态让人联想到 Serde 的 Deserialize trait。两者都是"从某种 source 产出 Self"的抽象。它们的相同和差异能互相照亮彼此的设计决策。
相同点:
- 都是"产出 Self"方向的 trait,和"把 Self 拿去哪"方向(
Serialize/IntoResponse)对称存在 - 都带关联 Rejection/Error 类型,失败由 trait 自己定义
- 都有 blanket impl 机制为通用类型(tuple、Option、Result)自动实现
差异点:
- Source 的类型:Serde 的 source 是 trait object
Deserializer(泛型 visitor pattern),运行时多态;Axum 的 source 是具体类型&mut Parts/Request,编译期单态化。两者的选择反映不同用途:Serde 要跨 JSON/YAML/CBOR 等多种格式,需要运行时抽象;Axum 只面向 HTTP 请求,没有跨格式需求。 - 消费 vs 借用:Serde 的
Deserializer总是按值被消费掉的;Axum 分FromRequestParts(借用)和FromRequest(消费)两种——因为 HTTP body 的"一次性"物理约束,而 Serde 的 source 在内存里可以 seek、回放。 - Rejection vs Error:Serde 的 Error 是通用错误类型
D::Error;Axum 的 Rejection 必须IntoResponse——和 HTTP 响应语义对齐,是产品语义驱动的类型选择。
在《Serde 元编程》第 5 章我们会看到 Deserializer 作为运行时 trait object 的设计如何影响自动派生宏的展开——和 axum-macros 的 FromRequest derive 形成鲜明对比。两个宏都是"给结构体自动生成 trait impl"的 derive,但底层 trait 的静态/动态本质决定了宏展开的复杂度差异。第 19 章讲 axum-macros 时会把这个对比收紧。
常见错误模式与排查
一旦理解了分裂的根本原因,大多数编译错误的诊断就变得直接。列出几类高频错误和它们的根治方案。
错误一:非最后参数实现了 FromRequest 而非 FromRequestParts。举例:
rust
async fn handler(Json(a): Json<A>, Path(id): Path<u64>) { /* ... */ }Json<A> 原生实现了 FromRequest<S, ViaRequest>——消费 body。它出现在非最后位置,第 5 章的宏展开需要第 1 个参数实现 FromRequestParts<S>,而 Json<A> 没有这个 impl。编译错误类似"Json<A>: FromRequestParts<S> is not satisfied"。修法:交换位置,让 Json 成为最后一个:async fn handler(Path(id): Path<u64>, Json(a): Json<A>)。语义上这也更贴近"先定位路径、再解析 body"的自然顺序。
错误二:两个 FromRequest 参数。举例:
rust
async fn handler(Json(a): Json<A>, Form(b): Form<B>) { /* ... */ }Json<A> 和 Form<B> 都是原生 FromRequest——两个都要消费 body。编译错误和错误一一样——因为除最后一个外所有参数都需要是 FromRequestParts,倒数第二个的 Json<A> 撞上这个约束。修法:业务上决定到底接受哪一种 body,或者实现一个复合提取器,先 peek Content-Type、再分发到 JSON / Form 的反序列化路径。
错误三:Rejection 类型忘实现 IntoResponse。自定义提取器时容易:
rust
enum MyRejection { /* ... */ }
// 没有 impl IntoResponse for MyRejection编译错误:"MyRejection: IntoResponse is not satisfied",直接点出缺失的 bound。修法:实现 IntoResponse,决定每个 variant 映射到的 HTTP 状态码。
错误四:Option<T> 用在没实现 OptionalFromRequest* 的类型上。Option<Path<u64>> 编译失败,因为 Path 没有 OptionalFromRequestParts 的 impl(Path 通常总是"存在且有效",不提供"可选"语义)。修法:改用 Result<Path<u64>, PathRejection>——Result 对任何 FromRequestParts 类型都有 blanket impl。
错误五:#[derive(FromRef)] 字段非 Clone。FromRef<T> 的 blanket impl 依赖 T: Clone,derive 也假设每个字段 clonable。字段带 std::sync::Mutex<T>(非 Clone)时 derive 失败。修法:把字段换成 Arc<Mutex<T>> 或 tokio::sync::Mutex(都是 Clone),或者手写 FromRef impl 改变 clone 语义。
这五类错误覆盖了用户绝大多数"为什么我的 handler 编译不过"的情形。根治办法是记住分裂原则:"消费 body 的只能有一个、且必须在最后"。其他规则都是对这条根本约束的类型化。
本章回到 Handler 章留下的伏笔
第 5 章的 impl_handler! 宏展开里有这几行关键代码:
rust
// 第 5 章回顾(axum/src/handler/mod.rs 宏展开)
let T1 = T1::from_request_parts(&mut parts, &state).await?;
let T2 = T2::from_request_parts(&mut parts, &state).await?;
// ...
let req = Request::from_parts(parts, body);
let T_last = T_last::from_request(req, &state).await?;现在每一行都有了依据:
from_request_parts的签名(&mut Parts, &S)来自FromRequestParts<S>trait 的定义- 多个
from_request_parts依次调用不冲突——因为它们都只要求&mut Parts,连续调用时 Rust 借用检查器允许 Request::from_parts(parts, body)重建请求——只有最后一次,因为 body 被立即消费from_request的签名(Request, &S)来自FromRequest<S, M>trait——M在编译期已被推导成ViaParts或ViaRequest- 返回值的
await?依赖 Rejection 的IntoResponsebound——err.into_response()直接可用
整章下来的核心信条只有一条:HTTP body 的物理一次性消费被 Rust 类型系统编译时强制。Axum 没有运行时检查"这个请求的 body 被读过了没"——它甚至没有这种概念。身份 by construction(by trait)就是正确的:如果你写下的 handler 签名能通过编译,那运行时一定不会有"两个提取器抢 body"的错误。
下一章我们深入内置提取器——Path<T>、Query<T>、State<S>、Json<T> 这四个最常用的。每个都是 FromRequestParts 或 FromRequest 的具体实现,我们看看它们如何用 extensions 通道、serde 集成、以及类型状态约束完成各自的工作。