Rust in Everything
基于 axum 0.8.9

Path、Query 与请求元数据

主线位置:请求主线。PathRouter 用 matchit 匹配路径时,顺手往请求的 extensions 里放了几样东西:解码后的路径参数、匹配到的路由模板、原始 URI。本页的提取器大多只是把它们取出来,再交给 serde。ConnectInfo 例外,它在装配主线上就要准备:axum::serve 每接受一条连接,IntoMakeServiceWithConnectInfo 把对端地址塞进这条连接上的每个请求。

flowchart TB
  req["请求进入 PathRouter"] --> ou["记 OriginalUri"]
  ou --> mt["matchit 匹配原始路径"]
  mt --> mp["记 MatchedPath"]
  mp --> up["解码参数存 UrlParams"]
  up --> ep["nest 层:剥前缀"]
  ep --> ex["handler 的提取器"]
  ex --> p1["Path:读 UrlParams"]
  ex --> p2["Query:读 uri.query()"]
  ex --> p3["元数据:读 extensions"]
  conn["每条连接:ConnectInfo"] --> ex
提取器从哪里取谁放进去取不到时
Path<T>extensions 里的 UrlParamsPathRouter::call_with_state500 No paths parameters found for matched route
Query<T> / RawQueryparts.uri.query()请求本身没有查询串按空串处理
MatchedPathextensions同上(feature matched-path)500 No matched path found
NestedPathextensionsnest 加的 SetNestedPath 层500 The matched route is not nested
OriginalUriextensions同上(feature original-uri)退回当前 parts.uri,不会失败
ConnectInfo<T>extensionsIntoMakeServiceWithConnectInfo500 Missing request extension

matched-path、original-uri、query 都在 axum 的默认 feature 里。提取器的两个 trait 和 Option<T> 的语义见 FromRequestParts 与 FromRequest,这里只讲每个提取器自己的逻辑。

路径参数是怎么来的

PathRouter::call_with_state 匹配成功后:

        match self.node.at(parts.uri.path()) {
            Ok(match_) => {
                let id = *match_.value;

                if !IS_FALLBACK {
                    #[cfg(feature = "matched-path")]
                    crate::extract::matched_path::set_matched_path_for_request(
                        id,
                        &self.node.route_id_to_path,
                        &mut parts.extensions,
                    );
                }

                url_params::insert_url_params(&mut parts.extensions, match_.params);
                // ...

matchit 拿到的是 uri.path() 的原文,也就是还没解码的路径;它返回的参数值是原文里的切片。解码在 insert_url_params 里做:

    let params = params
        .iter()
        .filter(|(key, _)| !key.starts_with(super::NEST_TAIL_PARAM))
        .filter(|(key, _)| !key.starts_with(super::FALLBACK_PARAM))
        .map(|(k, v)| {
            if let Some(decoded) = PercentDecodedStr::new(v) {
                Ok((Arc::from(k), decoded))
            } else {
                Err(Arc::from(k))
            }
        })
        .collect::<Result<Vec<_>, _>>();

    match (current_params, params) {
        // ...
        (_, Err(invalid_key)) => {
            extensions.insert(UrlParams::InvalidUtf8InPathParam { key: invalid_key });
        }
        (Some(UrlParams::Params(current)), Ok(params)) => {
            current.extend(params);
        }
        (None, Ok(params)) => {
            extensions.insert(UrlParams::Params(params));
        }
    }
  • axum 自己注册的通配参数(nest_service 的 __private__axum_nest_tail_param、fallback 的 __private__axum_fallback)在这里被过滤掉,Path 看不到。
  • PercentDecodedStr::new 是 percent_decode(...).decode_utf8().ok():只解 %XX,不把 + 当空格;解出来不是合法 UTF-8 就返回 None。
  • 有一个参数解码失败,整组参数换成 InvalidUtf8InPathParam { key }。路由照样匹配、handler 照样被调用,只有提取 Path 的那一刻才报错。
  • 已经有 Params(请求先经过了外层路由器)就在后面追加:嵌套时外层和内层的参数合在一起,顺序是外层在前。

PathDeserializer:把参数列表交给 serde

Path<T> 的 from_request_parts 取出 UrlParams,然后 T::deserialize(de::PathDeserializer::new(params))。PathDeserializer 只有一个字段,url_params: &[(Arc<str>, PercentDecodedStr)],即按出现顺序排好的「名字、值」列表。serde 的 Deserializer 由目标类型决定调哪个方法,同一个列表于是有几种读法:

目标类型serde 调用怎么读个数要求
u32、String、bool 等标量parse_single_value! 生成的方法、deserialize_str取唯一一个值,str::parse必须正好 1 个
元组、元组结构体deserialize_tuple按位置,忽略名字必须等于元组长度
结构体deserialize_struct,转给 deserialize_map按名字配字段由 serde 判断缺字段
HashMap、BTreeMapdeserialize_map名字作键无
Vec<T>deserialize_seq按位置无
Option、字节串、identifier、ignored_anyunsupported_type!直接报错—

单个值的解析由宏统一生成:

macro_rules! parse_single_value {
    ($trait_fn:ident, $visit_fn:ident, $ty:literal) => {
        fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
        where
            V: Visitor<'de>,
        {
            if self.url_params.len() != 1 {
                return Err(PathDeserializationError::wrong_number_of_parameters()
                    .got(self.url_params.len())
                    .expected(1));
            }

            let value = self.url_params[0].1.parse().map_err(|_| {
                PathDeserializationError::new(ErrorKind::ParseError {
                    value: self.url_params[0].1.as_str().to_owned(),
                    expected_type: $ty,
                })
            })?;
            visitor.$visit_fn(value)
        }
    };
}

元组和结构体内部的每个值交给 ValueDeserializer,它记得当前值的名字或下标,解析失败时报 ParseErrorAtKey 或 ParseErrorAtIndex,错误信息里能看出是哪个参数。

每种错误回给客户端什么

所有失败都收进 ErrorKind,再由 FailedToDeserializePathParams 决定状态码和 body。下面的程序把八种 ErrorKind 各触发一次,用 oneshot 直接调 Router,不起端口(这种测法见 不起服务器测路由):

axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["full"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "0.1"
serde = { version = "1", features = ["derive"] }
use std::{collections::BTreeMap, net::IpAddr};
use axum::{Router, body::Body, extract::{Path, Query}, http::Request, routing::get};
use http_body_util::BodyExt;
use serde::Deserialize;
use tower::ServiceExt;

#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct Params { id: u32, name: String }

#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct Page { n: i32 }

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/tuple/{a}/{b}", get(|Path(p): Path<(u32, String)>| async move { format!("{p:?}") }))
    .route("/struct/{id}/{name}", get(|Path(p): Path<Params>| async move { format!("{p:?}") }))
    .route("/map/{a}/{b}", get(|Path(p): Path<BTreeMap<String, String>>| async move { format!("{p:?}") }))
    .route("/one/{a}", get(|Path(p): Path<u32>| async move { format!("{p:?}") }))
    .route("/two/{a}/{b}", get(|Path(p): Path<u32>| async move { format!("{p:?}") }))
    .route("/only-id/{id}", get(|Path(p): Path<Params>| async move { format!("{p:?}") }))
    .route("/ip/{ip}", get(|Path(p): Path<IpAddr>| async move { format!("{p:?}") }))
    .route("/opt/{a}", get(|Path(p): Path<Option<u32>>| async move { format!("{p:?}") }))
    .route("/str/{s}", get(|Path(p): Path<String>| async move { format!("{p:?}") }))
    .route("/page", get(|Query(p): Query<Page>| async move { format!("{p:?}") }));

  for uri in [
    "/tuple/7/bob",
    "/struct/7/bob",
    "/map/7/bob",
    "/one/abc",
    "/tuple/abc/bob",
    "/struct/abc/bob",
    "/ip/not-an-ip",
    "/only-id/7",
    "/two/1/2",
    "/opt/1",
    "/str/%FF",
    "/page?n=abc",
    "/page",
  ] {
    let req = Request::get(uri).body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    let status = res.status();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("{uri:<22} {} {}", status.as_u16(), String::from_utf8_lossy(&body));
  }
}

M2 Max、rustc 1.99.0,连跑 3 次,输出相同:

/tuple/7/bob           200 (7, "bob")
/struct/7/bob          200 Params { id: 7, name: "bob" }
/map/7/bob             200 {"a": "7", "b": "bob"}
/one/abc               400 Invalid URL: Cannot parse `abc` to a `u32`
/tuple/abc/bob         400 Invalid URL: Cannot parse value at index 0 with value `abc` to a `u32`
/struct/abc/bob        400 Invalid URL: Cannot parse `id` with value `abc` to a `u32`
/ip/not-an-ip          400 Invalid URL: Cannot parse `ip` with value `not-an-ip`: invalid IP address syntax
/only-id/7             400 Invalid URL: missing field `name`
/two/1/2               500 Wrong number of path arguments for `Path`. Expected 1 but got 2. Note that multiple parameters must be extracted with a tuple `Path<(_, _)>` or a struct `Path<YourParams>`
/opt/1                 500 Unsupported type `core::option::Option<u32>`
/str/%FF               400 Invalid URL: Invalid UTF-8 in `s`
/page?n=abc            400 Failed to deserialize query string: n: invalid digit found in string
/page                  400 Failed to deserialize query string: missing field `n`

对照 status 和 body_text:

ErrorKind触发它的请求状态码body
ParseError/one/abc:单个值 parse 失败400Invalid URL: + 描述
ParseErrorAtIndex/tuple/abc/bob:元组第 0 个400同上
ParseErrorAtKey/struct/abc/bob:字段 id400同上
DeserializeError/ip/not-an-ip:类型自己的 Deserialize 报错,带名字和值400同上
Message/only-id/7:serde 的自定义错误,如缺字段400同上
InvalidUtf8InPathParam/str/%FF400同上
WrongNumberOfParameters/two/1/2:路由两个参数,Path<u32> 只要一个500描述本身,不加前缀
UnsupportedType/opt/1:Path<Option<u32>>500同上

分界线很清楚:400 的六种都和请求里的值有关,换一个 URL 可能就成功;500 的两种只取决于路由模板和 T 这个类型,同一个路由上每个请求都会失败,是写代码的人的错,所以不说「Invalid URL」。

📝NOTE
冷知识:元组的参数个数在 0.8 之前只检查下限

#2931(首次发布于 axum-v0.8.0-alpha.1)把 deserialize_tuple 和 deserialize_tuple_struct 的检查改成了今天的 self.url_params.len() != len。它修的 issue #2930 说,此前「Path<String> checks that there is exactly one path parameter, but Path<(String, String)> only checks that there is a minimum of two path parameters. It will then use the first two parameters as the actual values and discard the rest.」提出者在工作中遇到过一次事故:嵌套路由的外层把一个常量段改成了参数("/thing/:param1/:param2" 变成 /:param0/:param1/:param2),handler 里的 Path<(String, String)> 没改,于是新加的 param0 被当成了 param1。issue 建议的改法就是把 < 换成 !=。CHANGELOG 把它列为 breaking change。上面的 /two/1/2 是同一类错误在标量上的表现;元组今天也会同样回 500。

百分号解码与 +

上面的 /str/%FF 说明了解码失败的结果。解码成功时有两个细节值得先猜一下。

❗IMPORTANT
动脑筋:同样一段 a%2Fb%20c+d,放在路径里用 Path<String> 取,和放在查询串里用 Query 取,各得到什么?/path/a%2Fb%20c+d 能匹配 /path/{s} 吗?
#[derive(Deserialize, Debug)]
struct Search { q: String }

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/path/{s}", get(|Path(s): Path<String>| async move { format!("{s:?}") }))
    .route("/query", get(|Query(p): Query<Search>, RawQuery(raw): RawQuery| async move {
      format!("{:?} raw={raw:?}", p.q)
    }));

  for uri in ["/path/a%2Fb%20c+d", "/query?q=a%2Fb%20c+d"] {
    let res = app.clone().oneshot(Request::get(uri).body(Body::empty()).unwrap()).await.unwrap();
    let status = res.status().as_u16();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("{uri:<22} {status} {}", String::from_utf8_lossy(&body));
  }
}
M2 Max、rustc 1.99.0,依赖同上。答案在本页最后。

Query 与 RawQuery

Query<T> 的全部逻辑在 try_from_uri:

    pub fn try_from_uri(value: &Uri) -> Result<Self, QueryRejection> {
        let query = value.query().unwrap_or_default();
        let deserializer =
            serde_urlencoded::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
        let params = serde_path_to_error::deserialize(deserializer)
            .map_err(FailedToDeserializeQueryString::from_err)?;
        Ok(Query(params))
    }
  • 没有查询串就当空串,不单独报错。空串反序列化成结构体时,必填字段缺失,于是上面 /page 得到 missing field n``。可有可无的参数写成 Option<_> 字段。
  • 解码交给 form_urlencoded::parse,这是 application/x-www-form-urlencoded 的规则,+ 是空格。Form 用的是同一套,见 body 提取器。
  • serde_path_to_error 包了一层,错误信息带上出错字段的路径:n: invalid digit found in string。
  • 失败一律是 FailedToDeserializeQueryString,400,body 是固定前缀 Failed to deserialize query string 加 : 加 serde 的错误。
  • try_from_uri 是公开的,中间件里手上只有 Uri 时也能用。

RawQuery 只是 parts.uri.query().map(|query| query.to_owned()):Option<String>,不解码,不会失败。

Query 没有实现 OptionalFromRequestParts,所以 Option<Query<T>> 在 0.8 里不能当参数,编译报的是 handler 那条:

error[E0277]: the trait bound `fn(std::option::Option<Query<Page>>) -> impl Future<Output = ()> {handler}: Handler<_, _>` is not satisfied
   --> src/bin/opt_query.rs:11:51
    |
 11 |   let _app: Router = Router::new().route("/", get(handler));
    |                                               --- ^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(std::option::Option<Query<Page>>) -> impl Future<Output = ()> {handler}`
    |                                               |
    |                                               required by a bound introduced by this call
    |
    = note: Consider using `#[axum::debug_handler]` to improve the error message
📝NOTE
冷知识:Option<Query<T>> 在 0.8.0-rc.1 里能用,正式版删掉了

0.8 引入 OptionalFromRequestParts 时(#2475,axum-v0.8.0-rc.1),Query 也有一个实现,CHANGELOG 写的是「Option<Path<T>> and Option<Query<T>> no longer swallow all error conditions」。试用 rc.1 的用户在 issue #3079 里报告:URL 里没有 consistency 参数时(查询串里有别的参数),Option<Query<ConsistencyParams>> 不是 None,而是 400 Failed to deserialize query string: missing field consistency``;同样的代码在 0.7 下能跑,因为那时 Option 吞掉所有错误。讨论的结论是把 Option 放进结构体字段、直接用 Query<_>,维护者的评价是这个实现「likely more confusing than useful」,「this issue just shows very clearly that it's a footgun (as was the previous behavior of Option<Query<_>>, by the way)」。#3088「Remove OptionalFromRequestParts impl for Query」关闭了这个 issue,axum-v0.8.0 正式版的 CHANGELOG 记为「breaking: Remove OptionalFromRequestParts impl for Query」。同一版里的 #3081 给 Query、Form 加上了 serde_path_to_error,错误信息从「Failed to deserialize query string: invalid digit found in string」变成带字段名的形式。

Path 保留了 Option 实现(OptionalFromRequestParts for Path),只有 WrongNumberOfParameters { got: 0, .. }(路由里根本没有参数)才变成 None,其余错误照常拒绝。

MatchedPath、NestedPath、OriginalUri 与 nest

这三个提取器回答的是同一类问题:请求在路由树里走了哪条路。区别在 nest 下才看得出来。

Router::nest 不会把子路由器当成一个整体挂上去,而是在 PathRouter::nest 里把子路由的每条路径拼上前缀,逐条注册进外层,每个端点套两层:

            let path = path_for_nested_route(prefix, inner_path);

            let layer = (
                StripPrefix::layer(prefix),
                SetNestedPath::layer(path_to_nest_at),
            );

nest_service 则只注册 {prefix}/{*__private__axum_nest_tail_param}(和 {prefix} 本身),子路由器作为一个 Service 整体收到剥掉前缀的请求,自己再匹配一次。三个提取器各自的来源:

  • MatchedPath:set_matched_path_for_request 在匹配时写入路由模板。如果 extensions 里已经有上一层的模板(nest_service 的情况),append_nested_matched_path 去掉上一层末尾的 /{*__private__axum_nest_tail_param} 再拼上这一层的。fallback 路由器匹配时不写(if !IS_FALLBACK)。
  • NestedPath:SetNestedPath 层写入 nest 时给的前缀(模板形式),多层嵌套时拼接。
  • OriginalUri:PathRouter::call_with_state 一开头 if req.extensions().get::<OriginalUri>().is_none() 才插入,所以总是最外层看到的那个 URI。而 Uri 提取器拿的是 parts.uri,已经被 StripPrefix 改写过。

同一个子路由器,分别用 nest 和 nest_service 挂上去:

use axum::{
  Router,
  body::Body,
  extract::{MatchedPath, NestedPath, OriginalUri, RawPathParams},
  http::{Request, Uri},
  routing::get,
};
use http_body_util::BodyExt;
use tower::ServiceExt;

async fn handler(
  m: MatchedPath,
  n: NestedPath,
  uri: Uri,
  OriginalUri(o): OriginalUri,
  params: RawPathParams,
) -> String {
  let params: Vec<_> = params.iter().collect();
  format!(
    "matched  = {}\n  nested   = {}\n  uri      = {uri}\n  original = {o}\n  params   = {params:?}",
    m.as_str(),
    n.as_str()
  )
}

#[tokio::main]
async fn main() {
  let users = || Router::new().route("/users/{id}", get(handler));
  let app = Router::new()
    .nest("/api/{version}", users())
    .nest_service("/svc", users());

  for uri in ["/api/v1/users/7?x=1", "/svc/users/7?x=1"] {
    let req = Request::get(uri).body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("GET {uri}\n  {}", String::from_utf8_lossy(&body));
  }
}

连跑 3 次,输出相同:

GET /api/v1/users/7?x=1
  matched  = /api/{version}/users/{id}
  nested   = /api/{version}
  uri      = /users/7?x=1
  original = /api/v1/users/7?x=1
  params   = [("version", "v1"), ("id", "7")]
GET /svc/users/7?x=1
  matched  = /svc/users/{id}
  nested   = /svc
  uri      = /users/7?x=1
  original = /svc/users/7?x=1
  params   = [("id", "7")]
  • 两种挂法的 MatchedPath 都是完整模板,适合当日志、指标的标签(不会因为 ID 不同而炸开基数)。nest 是外层一次匹配直接得到;nest_service 是外层的 /svc/{*...} 和内层的 /users/{id} 拼出来的。
  • Uri 都被剥掉了前缀,handler 看到的是相对子路由器的路径;要拼重定向地址、生成绝对链接时用 OriginalUri。
  • RawPathParams(Path 的不反序列化版本)里,nest 下外层的 version 和内层的 id 都在,外层在前:nest 把子路由拼上前缀压平进同一棵路由树,一次匹配就得到两个参数(见 nest、merge 与 fallback);nest_service 的外层只有被过滤掉的通配参数。

ConnectInfo:要换一个 make service

对端地址不在 HTTP 请求里,只有接受连接的那一刻知道。IntoMakeServiceWithConnectInfo 是一个「每条连接调用一次」的 make service,它的 call:

    fn call(&mut self, target: T) -> Self::Future {
        let connect_info = ConnectInfo(C::connect_info(target));
        let svc = Extension(connect_info).layer(self.svc.clone());
        ResponseFuture::new(ready(Ok(svc)))
    }

target 是 axum::serve 交来的 IncomingStream,SocketAddr 的 Connected 实现 取 *stream.remote_addr()。然后给这条连接的服务套一层 Extension:这条连接上的每个请求都会被插入同一个 ConnectInfo。ConnectInfo 提取器 就是 Extension::<Self> 查找,找不到再看有没有测试用的 MockConnectInfo,都没有就返回 Extension 的拒绝。axum::serve 如何调用 make service 见 hyper 卷 本站的服务端路径。

所以 ConnectInfo 用错了是运行时 500,不是编译错误。真起一个监听器,用 hyper 的客户端(不读系统代理)各请求一次:

axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["full"] }
hyper = { version = "=1.12.0", features = ["client", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http = "1"
http-body-util = "0.1"
use std::net::SocketAddr;
use axum::{Router, extract::ConnectInfo, routing::get};
use http_body_util::{BodyExt, Empty};
use hyper::body::Bytes;
use hyper_util::rt::TokioIo;
use tokio::net::{TcpListener, TcpStream};

async fn handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
  format!("client = {}", addr.ip())
}

async fn get_once(addr: SocketAddr) -> String {
  let io = TokioIo::new(TcpStream::connect(addr).await.unwrap());
  let (mut tx, conn) = hyper::client::conn::http1::handshake::<_, Empty<Bytes>>(io).await.unwrap();
  tokio::spawn(conn);
  let req = http::Request::get("/").header("host", "x").body(Empty::new()).unwrap();
  let res = tx.send_request(req).await.unwrap();
  let status = res.status().as_u16();
  let body = res.into_body().collect().await.unwrap().to_bytes();
  format!("{status} {}", String::from_utf8_lossy(&body))
}

#[tokio::main]
async fn main() {
  let app = Router::new().route("/", get(handler));

  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  tokio::spawn(axum::serve(listener, app.clone()).into_future());
  println!("serve(listener, app):\n  {}", get_once(addr).await);

  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  let make = app.into_make_service_with_connect_info::<SocketAddr>();
  tokio::spawn(axum::serve(listener, make).into_future());
  println!("serve(listener, app.into_make_service_with_connect_info()):\n  {}", get_once(addr).await);
}

连跑 3 次,输出相同:

serve(listener, app):
  500 Missing request extension: Extension of type `axum::extract::connect_info::ConnectInfo<core::net::socket_addr::SocketAddr>` was not found. Perhaps you forgot to add it? See `axum::Extension`.
serve(listener, app.into_make_service_with_connect_info()):
  200 client = 127.0.0.1

报错文字和 状态 一章里缺 Extension 时一模一样,因为它就是那个 MissingExtension。Router::into_make_service_with_connect_info 还会先调一次 with_state(()),把端点提前转成 Route。用 oneshot 测试时没有连接,同样会 500,要在测试里套一层 MockConnectInfo,见 不起服务器测路由。

动脑筋答案

连跑 3 次,输出相同:

/path/a%2Fb%20c+d      200 "a/b c+d"
/query?q=a%2Fb%20c+d   200 "a/b c d" raw=Some("q=a%2Fb%20c+d")
  • 能匹配。 matchit 匹配的是 uri.path() 原文,%2F 在它眼里是三个普通字符,不是分隔符,所以整段落在 {s} 一个参数里。解码在匹配之后。
  • Path 得到 a/b c+d:%2F 解成 /,%20 解成空格,+ 原样保留。PercentDecodedStr::new 只做百分号解码,路径里的 + 按 RFC 3986 就是加号。
  • Query 得到 a/b c d:form_urlencoded::parse 按表单编码的规则把 + 也解成空格。
  • RawQuery 是原文,什么都没解。

客户端如果用同一个函数编码路径段和查询值,带 + 的值在两处会得到不同结果;要在路径里传字面空格,编成 %20。

小结

  • 路由匹配时,PathRouter 往 extensions 里放 OriginalUri(只在最外层)、MatchedPath(非 fallback)和百分号解码后的 UrlParams;提取器只是取出来。
  • PathDeserializer 把参数列表按目标类型读成标量、元组、结构体或 map;标量和元组要求个数严格相等(元组从 0.8 起)。
  • 值解析失败回 400,body 以 Invalid URL: 开头;参数个数不对或类型不支持回 500,因为那是路由和代码不一致。
  • 路径只做百分号解码,+ 保留;Query 用 form_urlencoded,+ 变空格;非法 UTF-8 到提取 Path 时才报 400。
  • Query 没有 Option 实现,缺参数用 Option 字段表达;错误带字段路径,统一 400。
  • nest 下 Uri 被剥了前缀,OriginalUri 是原始的,MatchedPath 是完整模板。
  • ConnectInfo 来自每条连接的 make service,要 into_make_service_with_connect_info,否则运行时 500。

💬 关联讨论

发起讨论
加载中...