FromRequestParts 与 FromRequest
主线位置:请求主线上
Handler::call的内部,在调用你的函数之前;也决定装配主线的第一步能不能成立:参数类型不满足这两个 trait 之一,函数就不是Handler,get(handler)编译不过。
flowchart TB
req["Request"] --> split["into_parts"]
split --> parts["Parts:请求头部"]
split --> body["Body"]
parts --> p1["参数 1:FromRequestParts"]
p1 --> p2["参数 2:FromRequestParts"]
p2 --> join["from_parts 拼回 Request"]
body --> join
join --> last{"最后一个参数"}
last -->|ViaRequest| own["拿走请求,可读 body"]
last -->|ViaParts| via["只用 Parts,丢弃 body"]
两个 trait 都在 axum-core 里。axum-core 单独成 crate,是为了让写提取器的库只依赖这一小块;axum 用 pub use 原样导出(axum::extract::{FromRequest, FromRequestParts, OptionalFromRequest, OptionalFromRequestParts, Request})。调用它们的 Handler::call 见 Handler trait。
两个 trait
FromRequestParts 和 FromRequest,去掉文档注释:
#[diagnostic::on_unimplemented(
note = "Function argument is not a valid axum extractor. \nSee `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;
}
#[diagnostic::on_unimplemented(
note = "Function argument is not a valid axum extractor. \nSee `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<S> | FromRequest<S, M> | |
|---|---|---|
| 输入 | &mut Parts:借用,只有头部信息 | Request:按值,带 body |
| 一个 handler 里能有几个 | 任意多个,放在前面 | 最多一个,必须是最后一个参数 |
| 额外的类型参数 | 无 | M,默认 ViaRequest,下面讲 |
| 失败时 | Rejection: IntoResponse,直接变成响应 | 同左 |
几个共同点:
state: &S:两个 trait 都按引用拿状态。S是Router<S>的状态类型,State<T>提取器靠FromRef从里面取,见 State 与 FromRef。- 返回
impl Future + Send:方法签名用返回位置的impl Trait,实现时可以直接写async fn,本页所有自己写的提取器都是这样。+ Send写在签名里,所以提取器的 future 必须能跨线程。 Rejection: IntoResponse:拒绝本身就是一个响应。Handler::call拿到Err(rejection)就rejection.into_response()返回,不需要框架另设一套错误处理。on_unimplemented:参数类型不是提取器时,错误里会多一行「Function argument is not a valid axum extractor.」(在#[debug_handler]生成的检查里能看到,见后文)。
&mut Parts 是可变借用,所以一个 FromRequestParts 提取器可以改请求头或往 extensions 里放东西,后面的提取器看得到。Handler trait 一页的动脑筋程序里,Step<2> 看到了 Step<1> 写进 extensions 的记录。
body 为什么只能给最后一个参数
body 是一个只能读一次的异步流。Handler::call 先把请求拆开,前面的参数只拿 &mut Parts,body 一直留在一边;最后一个参数得到的是重新拼好的 Request,按值拿走,读不读 body 由它决定。这个顺序在类型上写死:宏生成的 impl 要求前面的参数实现 FromRequestParts<S>,最后一个实现 FromRequest<S, M>。axum 的提取器文档「The order of extractors」一节(docs/extract.md 第 120 行)是这么说的:
The request body is an asynchronous stream that can only be consumed once.
Therefore you can only have one extractor that consumes the request body. axum
enforces this by requiring such extractors to be the _last_ argument your
handler takes.
把 String 放在第一个参数:
axum = { version = "=0.8.9", default-features = false }
use axum::{Router, http::Method, routing::post};
async fn handler(body: String, method: Method) -> String {
format!("{method} {body}")
}
fn main() {
let _app: Router = Router::new().route("/", post(handler));
}
rustc 1.99.0:
error[E0277]: the trait bound `fn(String, Method) -> impl Future<Output = String> {handler}: Handler<_, _>` is not satisfied
--> src/bin/body_first.rs:8:52
|
8 | let _app: Router = Router::new().route("/", post(handler));
| ---- ^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(String, Method) -> impl Future<Output = String> {handler}`
| |
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
note: required by a bound in `post`
--> axum-0.8.9/src/routing/method_routing.rs:167:16
|
167 | H: Handler<T, S>,
| ^^^^^^^^^^^^^ required by this bound in `post`
...
445 | top_level_handler_fn!(post, POST);
| ---------------------------------
| | |
| | required by a bound in this function
| in this macro invocation
= note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
真正的原因是 String: FromRequestParts<()> 不成立(String 只实现了 FromRequest),于是唯一可能匹配两参数函数的 impl 不适用。但编译器只报到 Handler<_, _> 这一层,不往下说。给函数加上 #[axum::debug_handler](需要 axum 的 macros feature)并用 debug 构建,第一条错误变成:
error: `String` consumes the request body and thus must be the last argument to the handler function
--> src/bin/body_first_debug.rs:4:24
|
4 | async fn handler(body: String, method: Method) -> String {
| ^^^^^^
这条信息不是类型检查得出的。debug_handler 在 #[cfg(not(debug_assertions))] 下原样返回输入,--release 构建里它什么也不做;debug 构建里它按类型名字比对一张表 request_consuming_type_name(Json、RawForm、Multipart、Form、Request、Bytes、String、Parts 等),名字在表里又不是最后一个参数,就生成上面的 compile_error!。自己写的 body 提取器不在表里,只能得到普通的 trait 不满足错误。各种 Handler<_, _> 报错的成因放在「常见坑」部分逐个复现。
axum 0.5 的 FromRequest 签名是 async fn from_request(req: &mut RequestParts<B>),RequestParts 里的 body 是一个 body: Option<B>,哪个提取器都可以把它取走;取走之后再来取的提取器会失败,得到 BodyAlreadyExtracted 拒绝。#1272(标题「Only allow last extractor to mutate the request」,修的是 issue #1115「Idea: Split FromRequest trait into three (like the Fn traits)」)把它拆成了今天的两个 trait。axum 0.6.0 的 CHANGELOG 原文:「It is now only possible for one extractor per handler to consume the request body. In 0.5 doing so would result in runtime errors but in 0.6 it is a compile error」,以及「axum enforces this by only allowing the last extractor to consume the request.」同一版本「BodyAlreadyExtracted has been removed」。
提交 be624306 第一次出现在 axum 0.6.0-rc.1(axum-core 0.3.0-rc.1),正式版是 axum 0.6.0。本页 ViaParts / ViaRequest 这两个标记也是这个提交加的。
ViaParts:只看头的提取器也能放最后
宏要求最后一个参数实现 FromRequest。如果只有这一条,async fn handler(method: Method) 就不能编译,因为 Method 只实现了 FromRequestParts。axum-core 用一个 blanket impl 补上(第 91 行):
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, _) 把 body 直接丢掉:最后一个参数只要头部信息,body 就没人读了。
M 的两个取值是 private 模块里两个没有值的枚举:
mod private {
#[derive(Debug, Clone, Copy)]
pub enum ViaParts {}
#[derive(Debug, Clone, Copy)]
pub enum ViaRequest {}
}
自己实现 FromRequest 时不写 M,用的是默认的 ViaRequest;经由 blanket impl 得到的是 FromRequest<S, ViaParts>。Handler 的宏把这个 M 放进标记元组 (M, T1, ..., Tn),编译器推断时会选中两者里唯一成立的那个。模块是私有的,外部 crate 写不出 ViaParts,也就不能自己给 FromRequest<S, ViaParts> 写 impl。
问: 为什么非要多一个 M?直接写 impl<T: FromRequestParts<S>> FromRequest<S> for T 不行吗?
答: 会和别的 FromRequest 实现重叠。axum-core 自己就有一个:Result<T, T::Rejection> 同时实现了 FromRequestParts(当 T: FromRequestParts)和 FromRequest(当 T: FromRequest),见下一节。去掉 M,blanket impl 和 Result 的 FromRequest impl 都覆盖 Result<_, _>。用一个不依赖 axum 的最小版本(两个 trait、一个 blanket impl、Result 的两个 impl)试,rustc 1.99.0 报:
error[E0119]: conflicting implementations of trait `FromRequest` for type `Result<_, _>`
--> src/bin/no_via.rs:26:1
|
12 | impl<T: FromRequestParts> FromRequest for T {
| ------------------------------------------- first implementation here
...
26 | impl<T: FromRequest> FromRequest for Result<T, T::Rejection> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Result<_, _>`
有了 M,两者是 FromRequest<S, ViaParts> 和 FromRequest<S, ViaRequest>,两个不同的 trait,不再重叠。元组的 FromRequest impl 旁边也有一句注释说明同一件事(tuple.rs 第 46 行):「This impl must not be generic over M, otherwise it would conflict with the blanket implementation of FromRequest<S, Mut> for T: FromRequestParts<S>.」
包一层:Result 和元组
Result<T, T::Rejection>(第 107 行、第 119 行):
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)
}
}
Rejection = Infallible:外层永远成功,内层的成败原样交给你的函数。想自己决定拒绝时回什么,就用它。
元组:impl_from_request! 经 axum-core 的 all_the_tuples! 给 1–16 元组实现两个 trait,() 另有一个 Infallible 的实现。规则和 handler 的参数一样:元组里前面的元素要 FromRequestParts,FromRequest 版本的最后一个元素可以拿 body。元组的 Rejection 是 Response,每一步 .map_err(|err| err.into_response())?,第一个失败的元素决定响应。所以 (A, B, C) 当一个参数用,行为和三个参数相同,只占一个参数位置。
Option<T>:0.8 的 OptionalFromRequestParts
axum-core 0.5(随 axum 0.8)给 Option<T> 换了实现。之前的实现在提交 ec75ee38 里被删掉,原文是:
impl<S, T> FromRequestParts<S> for Option<T>
where
T: FromRequestParts<S>,
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Option<T>, Self::Rejection> {
Ok(T::from_request_parts(parts, state).await.ok())
}
}
.ok():任何错误都变成 None。客户端发来格式错的参数,handler 看到的和「没有这个参数」一样。
现在 Option<T> 要求 T 实现单独的 OptionalFromRequestParts(作最后一个参数时还可以走 OptionalFromRequest):
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;
}
#[diagnostic::do_not_recommend]
impl<S, T> FromRequestParts<S> for Option<T>
where
T: OptionalFromRequestParts<S>,
S: Send + Sync,
{
type Rejection = T::Rejection;
#[allow(clippy::use_self)]
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Option<T>, Self::Rejection>> {
T::from_request_parts(parts, state)
}
}
返回值是 Result<Option<Self>, Rejection>:什么时候是 None、什么时候拒绝,由每个类型自己定。axum 0.8.9 里的实现:
| 类型 | trait | None | 拒绝 |
|---|---|---|---|
Path<T> | OptionalFromRequestParts | 匹配到的路由没有路径参数(WrongNumberOfParameters { got: 0, .. }) | 其他所有 PathRejection,比如解析失败 |
Extension<T> | OptionalFromRequestParts | extensions 里没有 T | 无,Rejection = Infallible |
MatchedPath | OptionalFromRequestParts | extensions 里没有 | 无,Infallible |
Json<T> | OptionalFromRequest | 请求没有 Content-Type 头 | 有 Content-Type 但不是 JSON,或 body 解析失败 |
Multipart | OptionalFromRequest | 没有 Content-Type,或不是 multipart | boundary 无效 |
Query<T> 不在表里。它只实现了 FromRequestParts,所以 Option<Query<T>> 在 0.8.9 里编译不过:
axum = { version = "=0.8.9", default-features = false, features = ["query"] }
serde = { version = "1", features = ["derive"] }
use axum::{Router, extract::Query, routing::get};
use serde::Deserialize;
#[derive(Deserialize)]
struct Page {
n: u32,
}
async fn list(page: Option<Query<Page>>) -> String {
format!("{:?}", page.map(|Query(p)| p.n))
}
fn main() {
let _app: Router = Router::new().route("/", get(list));
}
不加 #[debug_handler] 时,错误和上面一样只说 Handler<_, _> 不满足。加上(debug 构建)之后,第一条错误点到了参数本身(help 里列出的 9 个类型省略):
error[E0277]: the trait bound `std::option::Option<Query<Page>>: FromRequest<(), _>` is not satisfied
--> src/bin/option_query_debug.rs:10:21
|
10 | async fn list(page: Option<Query<Page>>) -> String {
| ^^^^^^ the trait `FromRequest<(), _>` is not implemented for `std::option::Option<Query<Page>>`
|
= note: Function argument is not a valid axum extractor.
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details
Option<Query<T>> 在 rc 里能用,正式版删掉了#2475「Add a separate trait for optional extractors」解决的是 issue #2298,那里写着:「The possibility of using Option<Extractor> as an extractor has lead to a bunch of confusion because people have expected or wanted it to have different semantics for specific inner extractors.」它删掉了上面那个 .ok() 实现,加了 OptionalFromRequest / OptionalFromRequestParts,并给 Path、Query、MatchedPath 等实现了它们。axum-core 的 CHANGELOG:「Option<T> as an extractor now requires T to implement the new trait OptionalFromRequest (if used as the last extractor) or OptionalFromRequestParts (other extractors)」。首次发布在 axum 0.8.0-rc.1 / axum-core 0.5.0-rc.1,axum 那一条写的是「Option<Path<T>> and Option<Query<T>> no longer swallow all error conditions」。
rc.1 里 Option<Query<T>> 的规则是「没有 query string 才是 None」。issue #3079 是试用 rc.1 时的报告:handler 用 Option<Query<ConsistencyParams>>,URL 里有别的参数(?expand=mainProduct&expand=order)但没有 consistency,请求被拒绝成 400,信息是「Failed to deserialize query string: missing field consistency」。报告里的理由是「A handler can define several query parameters, and maybe all of them are optional.」#3088「Remove OptionalFromRequestParts impl for Query」随即删掉了这个实现,axum 0.8.0 的 CHANGELOG 在「since rc.1」下记了一条「breaking: Remove OptionalFromRequestParts impl for Query」,同时正式版的总表里那一条也改成了只提 Option<Path<T>>。所以从 0.8.0 正式版起,Option<Query<T>> 就是上面那个编译错误。
想区分「没传」和「传错了」,用 Result<Query<T>, QueryRejection>。下面的动脑筋把这几种写法放在一起跑。Path、Query 自己的错误分类见 Path、Query 与请求元数据,Json 的 rejection 见 body 提取器。
Option<Path<u32>> 遇到 /p/abc 回什么状态码?axum = { version = "=0.8.9", default-features = false, features = ["query"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "0.1"
serde = { version = "1", features = ["derive"] }
use axum::{
Extension, Router,
body::Body,
extract::{Path, Query, Request, rejection::{PathRejection, QueryRejection}},
routing::get,
};
use http_body_util::BodyExt;
use serde::Deserialize;
use tower::ServiceExt;
#[derive(Deserialize)]
struct Page {
n: u32,
}
#[derive(Clone)]
struct User;
async fn opt_path(id: Option<Path<u32>>) -> String {
format!("Option<Path> = {:?}", id.map(|Path(v)| v))
}
async fn res_query(page: Result<Query<Page>, QueryRejection>) -> String {
match page {
Ok(Query(p)) => format!("Result<Query> = Ok({})", p.n),
Err(e) => format!("Result<Query> = Err({})", e.body_text()),
}
}
async fn res_path(id: Result<Path<u32>, PathRejection>) -> String {
match id {
Ok(Path(v)) => format!("Result<Path> = Ok({v})"),
Err(e) => format!("Result<Path> = Err({})", e.body_text()),
}
}
async fn opt_ext(user: Option<Extension<User>>) -> String {
format!("Option<Extension> is_some = {}", user.is_some())
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let app = Router::new()
.route("/p", get(opt_path))
.route("/p/{id}", get(opt_path))
.route("/r/{id}", get(res_path))
.route("/q", get(res_query))
.route("/e", get(opt_ext));
for uri in ["/p/7", "/p/abc", "/p", "/r/abc", "/q?n=3", "/q?n=x", "/q", "/e"] {
let req = Request::get(uri).body(Body::empty()).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let status = res.status().as_u16();
let body = res.into_body().collect().await.unwrap().to_bytes();
println!("{uri:8} {status} {}", String::from_utf8_lossy(&body));
}
}
同一个 opt_path 挂在 /p 和 /p/{id} 两个路由上。按 0.7 的 .ok() 语义,/p/abc 会是 200 和 None。0.8.9 呢?
现成的提取器
axum-core 的 request_parts.rs 给 http crate 的类型实现了这两个 trait:
| 类型 | trait | 做什么 | Rejection |
|---|---|---|---|
Request | FromRequest | 原样拿走整个请求 | Infallible |
Method、Uri | FromRequestParts | clone 出来 | Infallible |
Version | FromRequestParts | 复制(Copy) | Infallible |
HeaderMap | FromRequestParts | clone 整个头表 | Infallible |
Parts、Extensions | FromRequestParts | clone 整个 Parts 或 extensions | Infallible |
Body | FromRequest | 拿走 body,不读 | Infallible |
Bytes、BytesMut、String | FromRequest | 读完整个 body(受 DefaultBodyLimit 限制);String 再检查 UTF-8 | BytesRejection / StringRejection |
FromRequestParts 只给了 &mut Parts,想把东西拿出来只能 clone,所以 HeaderMap、Parts 这类提取器每次都复制一整份。只要一个头的话,在自己的提取器里读 parts.headers 更省。
axum 自己的提取器:Extension<T>(extension.rs,从 extensions 里按类型取一份 clone,没有就拒绝,信息是「Extension of type ... was not found. Perhaps you forgot to add it? See axum::Extension.」);State 见 State 与 FromRef;Path、Query、MatchedPath、OriginalUri、ConnectInfo 见 Path、Query 与请求元数据;Json、Form、Multipart 和 body 大小限制见 body 提取器;WebSocketUpgrade 见 WebSocketUpgrade。
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
/p/7 200 Option<Path> = Some(7)
/p/abc 400 Invalid URL: Cannot parse `abc` to a `u32`
/p 200 Option<Path> = None
/r/abc 200 Result<Path> = Err(Invalid URL: Cannot parse `abc` to a `u32`)
/q?n=3 200 Result<Query> = Ok(3)
/q?n=x 200 Result<Query> = Err(Failed to deserialize query string: n: invalid digit found in string)
/q 200 Result<Query> = Err(Failed to deserialize query string: missing field `n`)
/e 200 Option<Extension> is_some = false
答案是 400。
/p/7、/p:Some(7)和None。/p这条路由没有路径参数,Path报WrongNumberOfParameters { got: 0, .. },Path的OptionalFromRequestParts只把这一种情况变成None。/p/abc:有参数但解析失败,Option<Path<u32>>把PathRejection原样返回,handler 没被调用,响应是 rejection 的 400。0.7 的.ok()在这里会给None。/r/abc:同样的失败,换成Result<Path<u32>, PathRejection>,外层不拒绝,handler 拿到Err自己决定,这里回了 200。/q?...、/q:Result<Query<Page>, QueryRejection>能分出成功、值错(n: invalid digit found in string)和缺字段(missing fieldn``),三种都由 handler 处理。/e:Option<Extension<User>>的Rejection是Infallible,没有扩展就是None,永远不会拒绝。
小结
FromRequestParts<S>借&mut Parts,可以有任意多个,依次共享同一个Parts;FromRequest<S, M>按值拿走Request,只能是最后一个参数。两者的Rejection都实现IntoResponse。- body 只能读一次,所以只有最后一个参数能拿到它;放错位置只会报
Handler<_, _>不满足,#[debug_handler]在 debug 构建里按类型名给出更直接的提示。 M(ViaParts/ViaRequest)让FromRequestParts类型经由 blanket impl 也能放最后(body 被丢弃),同时避免与Result等的FromRequest实现冲突(E0119)。Result<T, T::Rejection>永不拒绝,把成败交给函数;元组把多个提取器合成一个参数,第一个失败的元素决定响应。- 0.8 起
Option<T>需要T: OptionalFromRequestParts(或OptionalFromRequest),None与拒绝的边界由类型自己定;Path解析失败会拒绝,Query没有实现,Option<Query<T>>编译不过。