the trait bound Option<Query<Page>>: FromRequest<(), _> is not satisfied
主线位置:报错出在装配主线,
get(list)要求list实现Handler,而Handler的宏实现要求每个参数是提取器。改好之后的行为在请求主线上:参数缺失、格式错误时,是进 handler 还是直接回 400,由你选的写法决定。
flowchart TB arg["Option Query T"] --> need["要 Optional 实现"] need -->|"Query 没有"| fail["Handler 不满足"] arg2["Result 包住 Query"] --> ok1["handler 处理错误"] arg3["字段全 Option"] --> ok2["缺参数是 None"]
标题是加了 #[debug_handler] 之后的报错(rustc 原文写的是 std::option::Option)。不加的话,报的是另一句,下面两种都给出来。
复现
0.7 里常见的写法,「有查询参数就用,没有就算了」:
axum = "=0.8.9"
serde = { version = "1", features = ["derive"] }
use axum::{extract::Query, routing::get, Router};
use serde::Deserialize;
#[derive(Deserialize)]
struct Page {
page: u32,
}
async fn list(page: Option<Query<Page>>) -> String {
format!("{:?}", page.map(|Query(p)| p.page))
}
fn main() {
let _app: Router = Router::new().route("/", get(list));
}
rustc 1.99.0 的报错(只去掉了路径前缀):
error[E0277]: the trait bound `fn(std::option::Option<Query<Page>>) -> impl Future<Output = String> {list}: Handler<_, _>` is not satisfied
--> src/bin/oq_bad.rs:14:51
|
14 | let _app: Router = Router::new().route("/", get(list));
| --- ^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(std::option::Option<Query<Page>>) -> impl Future<Output = String> {list}`
| |
| 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 `axum::routing::get`
--> axum-0.8.9/src/routing/method_routing.rs:167:16
|
167 | H: Handler<T, S>,
| ^^^^^^^^^^^^^ required by this bound in `get`
...
441 | top_level_handler_fn!(get, GET);
| -------------------------------
| | |
| | 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)
这是「Handler<_, _> 不满足」的通用形式,它不说哪个参数有问题(各种原因见 handler-not-satisfied)。照 note 的建议给 list 加上 #[debug_handler](axum 开 macros feature),用 debug 构建:
error[E0277]: the trait bound `std::option::Option<Query<Page>>: FromRequest<(), _>` is not satisfied
--> src/bin/oq_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
= help: the following other types implement trait `FromRequest<S, M>`:
Body
Form<T>
Json<T>
RawForm
Result<T, <T as FromRequest<S>>::Rejection>
String
axum::body::Bytes
axum::http::Request<Body>
note: required by a bound in `__axum_macros_check_list_0_from_request_check`
--> src/bin/oq_debug.rs:10:21
|
10 | async fn list(page: Option<Query<Page>>) -> String {
| ^^^^^^ required by this bound in `__axum_macros_check_list_0_from_request_check`
后面还跟着一个和上面一样的 Handler<_, _> 错误。用 --release 构建时只有那一个:#[debug_handler] 在 release 下原样返回函数,不生成检查代码(源码地图)。
源码:Option 只转发给 OptionalFromRequest*
报错说的是 FromRequest,因为 page 是最后一个参数,Handler 的宏实现要求最后一个参数实现 FromRequest(其余的要 FromRequestParts,见 Handler trait)。Option<T> 的两个实现都在 axum-core 的 extract/option.rs,FromRequestParts 那个是:
// Compiler hint just says that there is an impl for Option<T>, not mentioning
// the bounds, which is not very helpful.
#[diagnostic::do_not_recommend]
impl<S, T> FromRequestParts<S> for Option<T>
where
T: OptionalFromRequestParts<S>,
S: Send + Sync,
{
type Rejection = T::Rejection;
// ...
}
FromRequest 那个同理,要求 T: OptionalFromRequest<S>。Option<T> 什么都不做,None 还是拒绝,全由 T 自己的 Optional* 实现决定。Query<T> 在 0.8.9 里只实现了 FromRequestParts,两个 Optional* 都没有,所以 Option<Query<T>> 哪个都不满足。最后一个参数本可以经 ViaParts 退回到 FromRequestParts,但那条路同样要求 Option<Query<T>>: FromRequestParts,绕回原处。
上面两行注释解释了 #[diagnostic::do_not_recommend]:不加的话,rustc 的 help 会说「Option<T> 有实现」,却不提 T 要满足什么,反而误导。所以 help 列表里没有 Option<T>,却有 Result<T, <T as FromRequest<S>>::Rejection>,这正是第一种替代写法。哪些类型实现了 Optional*、各自什么时候给 None,FromRequestParts 与 FromRequest 有一张表。
为什么删掉
0.8.0-rc.1 里 Query 有过 OptionalFromRequestParts 实现,规则是「URI 里没有查询串才是 None」:
if let Some(query) = parts.uri.query() {
let value = serde_urlencoded::from_str(query)
.map_err(FailedToDeserializeQueryString::from_err)?;
Ok(Some(Self(value)))
} else {
Ok(None)
}
试用 rc.1 的人在 issue #3079 里撞上的是:查询串里有别的参数、只是没有自己要的那个时,不是 None,而是 400「missing field」。查询串有没有,和某个参数有没有,是两回事;Option 套在整个提取器外面只能表达前者。#3088「Remove OptionalFromRequestParts impl for Query」删掉了这个实现,首次发布即 axum-v0.8.0 正式版。来龙去脉(包括 0.7 的 Option 吞掉所有错误)在 Path、Query 与请求元数据 和 FromRequestParts 与 FromRequest 的冷知识里,这里不重复。
结论是:「可选」应该写在字段上,而不是提取器上。
替代写法
三种写法,各发同样四个请求:没有查询串、page=2、只有别的参数、page 不是数字:
axum = "=0.8.9"
serde = { version = "1", features = ["derive"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::rejection::QueryRejection;
use axum::extract::{Query, Request};
use axum::routing::get;
use axum::Router;
use serde::Deserialize;
use tower::ServiceExt;
#[derive(Deserialize, Debug)]
struct Page {
page: u32,
}
// 1. Result: the handler decides what a bad or missing query means.
async fn with_result(q: Result<Query<Page>, QueryRejection>) -> String {
match q {
Ok(Query(p)) => format!("Ok(page={})", p.page),
Err(e) => format!("Err({})", e.body_text()),
}
}
// 2. Every field optional.
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct PageOpt {
page: Option<u32>,
}
async fn with_option_field(Query(p): Query<PageOpt>) -> String {
format!("{p:?}")
}
// 3. Plain fields with defaults from Default.
#[derive(Deserialize, Debug)]
#[serde(default)]
struct PageDefault {
page: u32,
per_page: u32,
}
impl Default for PageDefault {
fn default() -> Self {
Self { page: 1, per_page: 20 }
}
}
async fn with_default(Query(p): Query<PageDefault>) -> String {
format!("{p:?}")
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let app = Router::new()
.route("/result", get(with_result))
.route("/option", get(with_option_field))
.route("/default", get(with_default));
for path in ["/result", "/option", "/default"] {
for query in ["", "?page=2", "?other=1", "?page=abc"] {
let uri = format!("{path}{query}");
let res = app.clone().oneshot(Request::get(&uri).body(Body::empty()).unwrap()).await.unwrap();
let status = res.status().as_u16();
let body = axum::body::to_bytes(res.into_body(), 1024).await.unwrap();
println!("{uri:<18} {status} {}", String::from_utf8_lossy(&body));
}
}
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
/result 200 Err(Failed to deserialize query string: missing field `page`)
/result?page=2 200 Ok(page=2)
/result?other=1 200 Err(Failed to deserialize query string: missing field `page`)
/result?page=abc 200 Err(Failed to deserialize query string: page: invalid digit found in string)
/option 200 PageOpt { page: None }
/option?page=2 200 PageOpt { page: Some(2) }
/option?other=1 200 PageOpt { page: None }
/option?page=abc 400 Failed to deserialize query string: page: invalid digit found in string
/default 200 PageDefault { page: 1, per_page: 20 }
/default?page=2 200 PageDefault { page: 2, per_page: 20 }
/default?other=1 200 PageDefault { page: 1, per_page: 20 }
/default?page=abc 400 Failed to deserialize query string: page: invalid digit found in string
Result<Query<T>, QueryRejection>。 Result<T, T::Rejection> 对任何提取器都有实现(axum-core 的 FromRequestParts for Result),提取失败不再短路,而是把 QueryRejection 交给 handler。四个请求都进了 handler,状态码都是 200,由 handler 决定怎么回。缺参数和格式错误都是 Err,要区分只能看错误内容。想要 0.7 那种「出什么错都当没有」,就写 q.ok(),这时吞掉错误是你明着写出来的。
字段全 Option。 没有查询串时,Query::try_from_uri 用 value.query().unwrap_or_default() 拿到空串,照常反序列化,不需要 Option 套在外面。serde 派生对缺失的 Option 字段给 None,所以没有查询串和只有别的参数都是 None。格式错误仍然是 400,这是 0.8 想要的:「没给」和「给错了」分开。
#[serde(default)]。 加在结构体上,缺失的字段从 Default::default() 取,字段可以是 u32 而不是 Option<u32>,handler 里不用再 unwrap_or。格式错误同样 400。只想给个别字段默认值,用字段上的 #[serde(default)] 或 #[serde(default = "fn_name")]。
三种写法的选择:要在 handler 里自己处理坏参数(比如回一个带说明的 JSON 错误),用 Result;参数确实可有可无,用 Option 字段;有合理的默认值(分页),用 #[serde(default)]。另外,路径参数那边的 Option<Path<T>> 仍然能编译,但格式错误时是 400 而不是 None,见 FromRequestParts 与 FromRequest 的动脑筋。
?q=&page=。下面的 handler 收到 /search?q= 和 /search?q=&page=,各回什么?use axum::body::Body;
use axum::extract::{Query, Request};
use axum::routing::get;
use axum::Router;
use serde::Deserialize;
use tower::ServiceExt;
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct Search {
q: Option<String>,
page: Option<u32>,
}
async fn search(Query(s): Query<Search>) -> String {
format!("{s:?}")
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let app = Router::new().route("/search", get(search));
// What an HTML form with empty inputs submits.
for uri in ["/search?q=", "/search?q=&page="] {
let res = app.clone().oneshot(Request::get(uri).body(Body::empty()).unwrap()).await.unwrap();
let status = res.status().as_u16();
let body = axum::body::to_bytes(res.into_body(), 1024).await.unwrap();
println!("{uri:<17} {status} {}", String::from_utf8_lossy(&body));
}
}
动脑筋答案
连跑 3 次输出相同:
/search?q= 200 Search { q: Some(""), page: None }
/search?q=&page= 400 Failed to deserialize query string: page: cannot parse integer from empty string
q=是Some(""),不是None。 键在,值是空串。serde_urlencoded 0.7.1 对出现了的键,deserialize_option一律visit_some,不看值是不是空的;Option只在键根本不出现时才是None。page=是 400。 同样走visit_some,再把空串当u32解析,失败,Query拒绝,handler 没被调用。错误信息带着字段名page,来自serde_path_to_error。表单的空数字框就会触发这一条。- 要把空串当成「没给」,得自己处理:字段用
Option<String>接、在 handler 里过滤空串再解析,或者给字段写一个#[serde(deserialize_with = ...)];也可以用第一种写法Result<Query<T>, QueryRejection>接住错误。
小结
Option<Query<T>>在 0.8.9 编译不过:Option<T>只转发给T的OptionalFromRequestParts/OptionalFromRequest,Query两个都没实现。- 默认报错只说
Handler<_, _>不满足;加#[debug_handler]、用 debug 构建,才指到Option<Query<Page>>: FromRequest<(), _>。 - rc.1 曾有这个实现,「没有查询串才是
None」,缺单个参数时仍是 400,#3088 在 0.8.0 正式版前删掉了它。 - 替代:
Result<Query<T>, QueryRejection>让 handler 处理一切错误(.ok()即 0.7 的行为);字段全Option区分「没给」和「给错」;#[serde(default)]给缺失字段默认值。 ?page=不是「没给」:键在、值为空,Option<u32>字段照样 400。