MethodRouter
主线位置:两条主线都有。装配主线上,
get(handler)、.post(...)、on(filter, ...)把端点放进对应方法的槽,同时把方法名追加进一个预先拼好的Allow头;同一方法重复注册立即 panic。请求主线上,路径匹配之后,MethodRouter::call_with_state按请求方法挑槽,挑不到就交给 fallback,默认是 405。
flowchart TB
req["请求:路径已匹配"] --> hd{"方法是 HEAD"}
hd -->|是| hs{"head 槽有端点"}
hs -->|有| callh["调 head 端点"]
hs -->|没有| gs{"get 槽有端点"}
gs -->|有| callg["调 get 端点"]
hd -->|否| other{"对应方法的槽"}
other -->|有| callm["调该端点"]
gs -->|没有| fbk["fallback"]
other -->|"没有或未知方法"| fbk
fbk --> r405["默认:405"]
fbk --> allow["补 Allow 头"]
Router::route(path, method_router) 的第二个参数就是它。路径怎么匹配见 matchit 与路径语法;匹配到的端点怎么被调用、HEAD 的响应体怎么被去掉,见 Route 与类型擦除。
九个槽、一个 fallback、一个 Allow
MethodRouter 没有用表或 HashMap,每个方法一个字段:
pub struct MethodRouter<S = (), E = Infallible> {
get: MethodEndpoint<S, E>,
head: MethodEndpoint<S, E>,
delete: MethodEndpoint<S, E>,
options: MethodEndpoint<S, E>,
patch: MethodEndpoint<S, E>,
post: MethodEndpoint<S, E>,
put: MethodEndpoint<S, E>,
trace: MethodEndpoint<S, E>,
connect: MethodEndpoint<S, E>,
fallback: Fallback<S, E>,
allow_header: AllowHeader,
}
- 每个槽是
MethodEndpoint:None(没注册)、Route(已经是装箱的服务)、BoxedHandler(还欠着状态的 handler,with_state时才转成Route,见 状态:State 与 FromRef)。 fallback是Fallback枚举,MethodRouter::new放进去的默认值是一个只回StatusCode::METHOD_NOT_ALLOWED的service_fn。AllowHeader有三态:None(还没拼任何方法)、Skip(不要Allow头,any用)、Bytes(BytesMut)(拼好的值)。
九个固定字段意味着只认这九个标准方法。PROPFIND 之类的扩展方法没有槽,永远落到 fallback。MethodFilter 也只是九个位的 u16,MethodFilter::try_from(Method) 遇到其他方法返回 NoMatchingMethodFilter。
注册:填槽、拼 Allow
顶层的 get、post 等由 top_level_handler_fn! 生成,都是 on(MethodFilter::$method, handler);链式的 .get(...) 同理。最后都进 on_endpoint,它对九个槽各调一次内部函数 set_endpoint:
fn set_endpoint<S, E>(
method_name: &str,
out: &mut MethodEndpoint<S, E>,
endpoint: &MethodEndpoint<S, E>,
endpoint_filter: MethodFilter,
filter: MethodFilter,
allow_header: &mut AllowHeader,
methods: &[&'static str],
) where
MethodEndpoint<S, E>: Clone,
S: Clone,
{
if endpoint_filter.contains(filter) {
if out.is_some() {
panic!(
"Overlapping method route. Cannot add two method routes that both handle \
`{method_name}`",
)
}
*out = endpoint.clone();
for method in methods {
append_allow_header(allow_header, method);
}
}
}
set_endpoint(
"GET",
&mut self.get,
&endpoint,
filter,
MethodFilter::GET,
&mut self.allow_header,
&["GET", "HEAD"],
);
// ... HEAD、TRACE、PUT、POST、PATCH、OPTIONS、DELETE、CONNECT 同样各一次
三件事发生在这里:
- 一个过滤器可以填多个槽。
on(MethodFilter::PUT.or(MethodFilter::PATCH), h)让put和patch两个槽都指向同一个端点(各 clone 一份)。 - GET 槽带上 HEAD。 填
get槽时追加的是&["GET", "HEAD"],Allow里自动有HEAD;MethodFilter::GET的位并不包含HEAD,所以head槽仍然空着,可以另外注册。 Allow在启动时拼好。append_allow_header往BytesMut里追加,方法名,已经包含就跳过;Skip状态什么都不做。顺序就是set_endpoint的调用顺序加上注册的先后,不排序。
any(handler) 不填任何槽:它是 MethodRouter::new().fallback(handler).skip_allow_header(),把 handler 放在 fallback 位置,并把 Allow 设成 Skip。
分发:HEAD 先找 head,再找 get
call_with_state 用一个局部宏 call! 逐个比较方法:
call!(req, HEAD, head);
call!(req, HEAD, get);
call!(req, GET, get);
call!(req, POST, post);
call!(req, OPTIONS, options);
call!(req, PATCH, patch);
call!(req, PUT, put);
call!(req, DELETE, delete);
call!(req, TRACE, trace);
call!(req, CONNECT, connect);
let future = fallback.clone().call_with_state(req, state);
match allow_header {
AllowHeader::None => future.allow_header(Bytes::new()),
AllowHeader::Skip => future,
AllowHeader::Bytes(allow_header) => future.allow_header(allow_header.clone().freeze()),
}
call! 在方法相等且槽不为 None 时直接 return 端点的 future。HEAD 请求先看 head 槽,空的话再看 get 槽:这就是「get 自动处理 HEAD」的全部实现,与注册先后无关。显式注册的 head 端点总是优先。
所有槽都没接住,才走 fallback,并把启动时拼好的 Allow 交给返回的 RouteFuture。RouteFuture::poll 在响应出来后调用 set_allow_header:响应里还没有 Allow 时才插入。它不看状态码,只要走了 fallback 就补;AllowHeader::None(一个方法都没注册的 MethodRouter)补的是空值。
用一组路由把这几条都跑一遍:
axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["full"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "0.1"
use axum::{
Router,
body::Body,
http::{Method, Request, StatusCode},
routing::{MethodFilter, any, get, on},
};
use http_body_util::BodyExt;
use tower::ServiceExt;
async fn send(app: &Router, method: &str, uri: &str) {
let req = Request::builder()
.method(Method::from_bytes(method.as_bytes()).unwrap())
.uri(uri)
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let status = res.status().as_u16();
let allow = res.headers().get("allow").map(|v| v.to_str().unwrap().to_owned());
let len = res.headers().get("content-length").map(|v| v.to_str().unwrap().to_owned());
let body = res.into_body().collect().await.unwrap().to_bytes();
println!(
"{method:<8} {uri:<6} {status} allow={allow:?} content-length={len:?} body={:?}",
String::from_utf8_lossy(&body)
);
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route(
"/a",
get(|| async {
println!(" (get handler runs)");
"hello from GET"
})
.post(|| async { "post" })
.delete(|| async { "delete" }),
)
.route(
"/b",
get(|| async { "hello from GET" }).head(|| async {
println!(" (head handler runs)");
(StatusCode::OK, [("x-head", "1")], "head body")
}),
)
.route("/c", on(MethodFilter::PUT.or(MethodFilter::PATCH), || async { "put or patch" }))
.route("/c", get(|| async { "get" }))
.route("/d", any(|| async { "any" }))
.route(
"/e",
get(|| async { "get" }).fallback(|m: Method| async move { format!("custom fallback for {m}") }),
);
send(&app, "GET", "/a").await;
send(&app, "HEAD", "/a").await;
send(&app, "PUT", "/a").await;
send(&app, "PROPFIND", "/a").await;
send(&app, "HEAD", "/b").await;
send(&app, "GET", "/b").await;
send(&app, "DELETE", "/c").await;
send(&app, "DELETE", "/d").await;
send(&app, "POST", "/e").await;
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同。这里先看前 8 行,/c 那一行留给本页的动脑筋,完整输出在最后:
(get handler runs)
GET /a 200 allow=None content-length=Some("14") body="hello from GET"
(get handler runs)
HEAD /a 200 allow=None content-length=Some("14") body=""
PUT /a 405 allow=Some("GET,HEAD,POST,DELETE") content-length=Some("0") body=""
PROPFIND /a 405 allow=Some("GET,HEAD,POST,DELETE") content-length=Some("0") body=""
(head handler runs)
HEAD /b 200 allow=None content-length=Some("9") body=""
GET /b 200 allow=None content-length=Some("14") body="hello from GET"
HEAD /a:没有head槽,get的 handler 真的跑了一遍,响应带着 GET 那份 body 的长度14,body 被丢掉。去 body、补Content-Length发生在RouteFuture里,见 Route 与类型擦除。PUT /a和扩展方法PROPFIND /a一样,都是 405,Allow是GET,HEAD,POST,DELETE:get带来GET,HEAD,之后按链式调用的顺序追加。HEAD /b:显式注册的headhandler 接住了,Content-Length是它自己那份 body 的长度9;GET 的 handler 没有运行。
问: HEAD 请求不要 body,get 的 handler 却从头跑到尾,查库、渲染模板都做了一遍,这不是白干吗?
答: 是白干,axum 选择了正确性而不是省事。HEAD 的响应头应该和 GET 一致,最可靠的办法就是真的执行 GET,再把 body 换成空的;Content-Length 在丢 body 之前按 size_hint 算好,所以客户端看到的长度和 GET 一样。如果某个 GET 很贵、而 HEAD 请求又多(比如被监控系统频繁探测),就单独注册一个 head handler:分发时 head 槽先于 get 槽被检查,上面 /b 的输出就是这样。
重复注册:启动时 panic
同一方法有两个端点,axum 不猜该用哪个,直接 panic。有三个入口,报错措辞不同:
axum = "=0.8.9"
use std::panic::{self, AssertUnwindSafe};
use axum::{
Router,
routing::{MethodFilter, MethodRouter, get, head, post},
};
fn try_build<T>(label: &str, f: impl FnOnce() -> T) {
match panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(_) => println!("{label}\n ok"),
Err(e) => {
let msg = e
.downcast_ref::<String>()
.cloned()
.or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default();
println!("{label}\n panic: {msg}");
}
}
}
fn main() {
panic::set_hook(Box::new(|_| {}));
let h = || async {};
try_build("get(h).get(h)", || -> MethodRouter { get(h).get(h) });
try_build("get(h).on(GET.or(POST), h)", || -> MethodRouter {
get(h).on(MethodFilter::GET.or(MethodFilter::POST), h)
});
try_build("get(h).merge(get(h))", || -> MethodRouter { get(h).merge(get(h)) });
try_build("route(\"/x\", get(h)).route(\"/x\", get(h))", || -> Router {
Router::new().route("/x", get(h)).route("/x", get(h))
});
try_build("route(\"/x\", get(h)).route(\"/x\", post(h).get(h))", || -> Router {
Router::new().route("/x", get(h)).route("/x", post(h).get(h))
});
try_build("get(h).fallback(h).merge(post(h).fallback(h))", || -> MethodRouter {
get(h).fallback(h).merge(post(h).fallback(h))
});
try_build("get(h).head(h)", || -> MethodRouter { get(h).head(h) });
try_build("head(h).get(h)", || -> MethodRouter { head(h).get(h) });
}
连跑 3 次,输出相同:
get(h).get(h)
panic: Overlapping method route. Cannot add two method routes that both handle `GET`
get(h).on(GET.or(POST), h)
panic: Overlapping method route. Cannot add two method routes that both handle `GET`
get(h).merge(get(h))
panic: Overlapping method route. Cannot merge two method routes that both define `GET`
route("/x", get(h)).route("/x", get(h))
panic: Overlapping method route. Handler for `GET /x` already exists
route("/x", get(h)).route("/x", post(h).get(h))
panic: Overlapping method route. Handler for `GET /x` already exists
get(h).fallback(h).merge(post(h).fallback(h))
panic: Cannot merge two `MethodRouter`s that both have a fallback
get(h).head(h)
ok
head(h).get(h)
ok
- 链式追加走
set_endpoint,报「Cannot add two method routes that both handle」。 MethodRouter::merge和Router::route走同一个merge_for_path,区别在path参数:merge传None,报「Cannot merge two method routes」;Router::route传路径,报「Handler forGET /xalready exists」。Router::route对已注册的路径不会再插一次 matchit:PathRouter::route先用path_to_route_id查到旧的MethodRouter,和新的合并(源码注释:「This makes.route("/", get(_)).route("/", post(_))work」)。所以.route("/x", get(a)).route("/x", post(b))是合法的,Allow也会合并(AllowHeader::merge用逗号拼接两边)。- 两边都有自定义 fallback 也不能合并。
get和head不冲突,顺序也无所谓:它们是两个槽。
Allow 是 0.4.6 才补上的RFC 9110 要求 405 响应带 Allow 头,列出这个资源支持的方法。axum 早期的 MethodRouter 只回 405。#733(2022-01-30 合并,CHANGELOG 记在 0.4.6 下:「Set Allow header when responding with 405 Method Not Allowed」;合并提交首次进入 axum-v0.5.0,0.4 分支上是反向移植的同名提交,首次发布于 axum-v0.4.6)补上了它。PR 描述交代了今天代码里的两个细节:「The header value is built up as a BytesMut to avoid allocations when creating the response」,即注册时就拼好、每次 405 只 clone().freeze();「The header is also set when the fallback is hit, unless the response generated by the fallback already contains the header」,即 set_allow_header 里那句 !headers.contains_key(header::ALLOW),让自定义 fallback 可以自己决定 Allow。
MethodRouter::fallback 与 any
405 只是默认的 fallback。MethodRouter::fallback 把它换成任意 handler,上面程序里的 /e 就是这样。输出的最后一行(见本页最后)是:
POST /e 200 allow=Some("GET,HEAD") content-length=Some("24") body="custom fallback for POST"
自定义 fallback 回了 200,Allow 照样被补上,因为 set_allow_header 不看状态码。不想要这个头,就在 fallback 的响应里自己设 Allow(哪怕是你认为正确的值),axum 不会覆盖。
any(handler) 也是用 fallback 实现的:DELETE /d 回 200 且没有 Allow,因为 AllowHeader::Skip。合并时只要有一边是 Skip,结果就是 Skip。
Router 上还有一个 method_not_allowed_fallback,给已经注册的每个 MethodRouter 调一次内部的 default_fallback:只有 fallback 还是默认值时才替换,自定义过的不动。它只作用于调用那一刻已经存在的路由,和 nest 一起用时顺序有讲究,见 nest、merge 与 fallback。
/c 先注册了 on(MethodFilter::PUT.or(MethodFilter::PATCH), ...),又用第二个 .route("/c", get(...)) 加了 GET。对 /c 发 DELETE,Allow 头的值是什么,方法按什么顺序排列?代码就是上面那个程序,M2 Max、rustc 1.99.0。答案在本页最后。动脑筋答案
分发程序的完整输出,连跑 3 次相同:
(get handler runs)
GET /a 200 allow=None content-length=Some("14") body="hello from GET"
(get handler runs)
HEAD /a 200 allow=None content-length=Some("14") body=""
PUT /a 405 allow=Some("GET,HEAD,POST,DELETE") content-length=Some("0") body=""
PROPFIND /a 405 allow=Some("GET,HEAD,POST,DELETE") content-length=Some("0") body=""
(head handler runs)
HEAD /b 200 allow=None content-length=Some("9") body=""
GET /b 200 allow=None content-length=Some("14") body="hello from GET"
DELETE /c 405 allow=Some("PUT,PATCH,GET,HEAD") content-length=Some("0") body=""
DELETE /d 200 allow=None content-length=Some("3") body="any"
POST /e 200 allow=Some("GET,HEAD") content-length=Some("24") body="custom fallback for POST"
答案是 PUT,PATCH,GET,HEAD:
on(PUT | PATCH, ...)进on_endpoint,九次set_endpoint的固定顺序是 GET、HEAD、TRACE、PUT、POST、PATCH、OPTIONS、DELETE、CONNECT,其中 PUT 和 PATCH 命中,所以先是PUT,PATCH,不是按or的书写顺序,也不是按字母。- 第二次
.route("/c", get(...))不插 matchit,而是merge_for_path,AllowHeader::merge把新的GET,HEAD用逗号接在后面。 - 整个过程都在启动时完成,405 时只是把这串字节原样放进响应。
小结
MethodRouter是九个固定的方法槽加一个 fallback 和一个预先拼好的Allow;扩展方法没有槽,总是落到 fallback。get填 GET 槽并往Allow里加GET,HEAD;HEAD 请求先找head槽,再找get槽,GET 的 handler 会完整执行,body 随后被丢弃。- 没有对应方法时调用 fallback,默认 405;只要走了 fallback,响应里没有
Allow就补上,不看状态码。 - 同一方法注册两次在启动时 panic,链式、
merge、Router::route三种入口措辞不同;同一路径多次route不同方法是合法的,会合并。 any是「fallback + 不要 Allow」;MethodRouter::fallback自定义 405;Router::method_not_allowed_fallback只替换还是默认值的 fallback。