Rust in Everything
基于 axum 0.8.9

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 的动脑筋。

❗IMPORTANT
动脑筋:HTML 表单里有两个空输入框,提交后是 ?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));
  }
}
依赖同上。M2 Max、rustc 1.99.0。答案在本页最后。

动脑筋答案

连跑 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。

💬 关联讨论

发起讨论
加载中...