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 里的 UrlParams | PathRouter::call_with_state | 500 No paths parameters found for matched route |
Query<T> / RawQuery | parts.uri.query() | 请求本身 | 没有查询串按空串处理 |
MatchedPath | extensions | 同上(feature matched-path) | 500 No matched path found |
NestedPath | extensions | nest 加的 SetNestedPath 层 | 500 The matched route is not nested |
OriginalUri | extensions | 同上(feature original-uri) | 退回当前 parts.uri,不会失败 |
ConnectInfo<T> | extensions | IntoMakeServiceWithConnectInfo | 500 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、BTreeMap | deserialize_map | 名字作键 | 无 |
Vec<T> | deserialize_seq | 按位置 | 无 |
Option、字节串、identifier、ignored_any | unsupported_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`
ErrorKind | 触发它的请求 | 状态码 | body |
|---|---|---|---|
ParseError | /one/abc:单个值 parse 失败 | 400 | Invalid URL: + 描述 |
ParseErrorAtIndex | /tuple/abc/bob:元组第 0 个 | 400 | 同上 |
ParseErrorAtKey | /struct/abc/bob:字段 id | 400 | 同上 |
DeserializeError | /ip/not-an-ip:类型自己的 Deserialize 报错,带名字和值 | 400 | 同上 |
Message | /only-id/7:serde 的自定义错误,如缺字段 | 400 | 同上 |
InvalidUtf8InPathParam | /str/%FF | 400 | 同上 |
WrongNumberOfParameters | /two/1/2:路由两个参数,Path<u32> 只要一个 | 500 | 描述本身,不加前缀 |
UnsupportedType | /opt/1:Path<Option<u32>> | 500 | 同上 |
分界线很清楚:400 的六种都和请求里的值有关,换一个 URL 可能就成功;500 的两种只取决于路由模板和 T 这个类型,同一个路由上每个请求都会失败,是写代码的人的错,所以不说「Invalid URL」。
#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 说明了解码失败的结果。解码成功时有两个细节值得先猜一下。
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));
}
}
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 fieldn``。可有可无的参数写成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
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。