Appearance
第3章 MethodRouter:HTTP 动词分发与 Allow 头
一个 PATCH 请求的旅程
你写了这样一行路由:
rust
Router::new().route("/users", get(list_users).post(create_user))这在 Axum 里看起来天经地义。GET 请求走 list_users,POST 请求走 create_user。但如果来了一个 PATCH 请求呢?一个 DELETE 请求呢?一个 HEAD 请求呢?
后端 API 的现实是:同一个路径通常要处理多个 HTTP 方法。REST 语义要求 /users 至少支持 GET 和 POST,/users/:id 至少支持 GET、PUT、PATCH、DELETE。而 HTTP 协议本身还定义了 HEAD、OPTIONS、TRACE 这些方法——客户端可能真的会发这些请求,你的服务必须做出正确的响应。
这个问题看起来简单——用一个 HashMap<Method, Handler> 就能分发。但真实场景远比这复杂:
HEAD 请求怎么办? RFC 9110 第 9.1 节明确指出,HEAD 请求的响应必须和 GET 完全一致,唯一区别是不返回 body。如果你注册了 GET handler,HEAD 请求应该自动被 GET 处理——还是需要用户显式注册 HEAD?
方法不匹配时返回什么? 404 Not Found 不对——路径是存在的,只是方法不对。正确答案是 405 Method Not Allowed,并且响应必须包含 Allow 头,告诉客户端这个路径支持哪些方法。Allow 头的值从哪来?更棘手的是,如果路径 /users 注册了 GET 和 POST,一个 DELETE 请求到达时应该返回 Allow: GET,HEAD,POST——但"HEAD 从哪来"又和第一个问题纠缠在一起。
any() 和显式方法注册能共存吗? any(handler) 意味着所有方法都走同一个 handler,这时候还需要 Allow 头吗?如果 any() 之后又 .post(other_handler),POST 走哪个?
合并时的冲突怎么处理? get(a).merge(post(b)) 合情合理,但 get(a).merge(get(b)) 呢?两个 GET handler 指向同一路径,编译期还是运行时报错?
同一个路径上 handler 和 service 能混用吗? get(handler).post_service(svc) 是否合法?handler 需要延迟注入 state,service 已经是就绪的 tower Service——两者的内部表示不同,但在同一个 MethodRouter 里能否共存?
这些问题加在一起,让"按 HTTP 方法分发请求"这件事远不是一行 match 能搞定的。Axum 的答案是 MethodRouter——一个 1723 行的结构体,是整个 axum 仓库里最长的文件,也是理解请求分发机制的核心。
MethodRouter 的结构:九个字段,九个方法
打开 routing/method_routing.rs,在 547-559 行你会看到 MethodRouter 的定义:
rust
// routing/method_routing.rs:547-559
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,
}九个 HTTP 方法,九个字段。这不是偷懒——这是最直接的映射。HTTP/1.1 规范定义了 8 个标准方法,加上后来 RFC 5789 定义的 PATCH,一共 9 个:
| 方法 | 幂等 | 安全 | body | RFC |
|---|---|---|---|---|
| GET | ✓ | ✓ | 无 | RFC 9110 §9.3.1 |
| HEAD | ✓ | ✓ | 无 | RFC 9110 §9.3.2 |
| POST | ✗ | ✗ | 有 | RFC 9110 §9.3.3 |
| PUT | ✓ | ✗ | 有 | RFC 9110 §9.3.4 |
| DELETE | ✓ | ✗ | 可选 | RFC 9110 §9.3.5 |
| PATCH | ✗ | ✗ | 有 | RFC 5789 |
| OPTIONS | ✓ | ✓ | 无 | RFC 9110 §9.3.7 |
| TRACE | ✓ | ✓ | 无 | RFC 9110 §9.3.8 |
| CONNECT | ✗ | ✗ | 无 | RFC 9110 §9.3.6 |
每个方法对应一个字段,注册 handler 就是给对应字段赋值。
两个泛型参数也有明确的含义。S 是状态类型——handler 可能通过 State<S> 提取器访问的应用状态。在 with_state 调用之前,S 代表"还需要提供的状态";调用之后,S 变成 ()(对于不需要状态的 handler)或者保持原类型。E 是错误类型——当 MethodRouter 包含 service 而非 handler 时,service 的错误类型会传播到这里。纯 handler 的 MethodRouter 的 E 始终是 Infallible,因为 handler 不可能失败。
为什么不做成 HashMap<Method, Handler> 或者 Vec<(MethodFilter, Handler)>?两个原因。
第一,编译期完备性。用九个命名字段,你在 call_with_state 里必须显式处理每个方法——漏掉任何一个编译器都会警告。如果用 HashMap,你永远无法在编译期保证"所有已注册的方法都被检查了"。这个选择和 Rust 生态里"用 enum 代替字符串键"的哲学一脉相承——字段名字是编译期就确定的知识,不应该退化成运行时的字符串匹配。
第二,零间接寻址。HashMap 的查找有哈希计算和冲突处理的开销,而命名字段的访问是直接的内存偏移量。对于"每个请求都要执行一次"的热路径来说,这个区别是有意义的——虽然以现代 CPU 的性能来看,HashMap 查找的开销微乎其微,但 Axum 选择了更确定性的方案。命名字段还有一个隐含的好处:编译器可以对每个字段的访问做独立的优化,而 HashMap 的访问每次都要走相同的查表逻辑。
结构体还有两个额外的字段:fallback 和 allow_header。fallback 处理"没有任何方法匹配"的情况,allow_header 则负责生成 405 响应中的 Allow 头。它们的故事我们稍后展开。
下面的图展示了 MethodRouter 的整体结构:
MethodEndpoint:三种状态
每个方法字段都是一个 MethodEndpoint<S, E>,定义在 routing/method_routing.rs:1272-1276:
rust
// routing/method_routing.rs:1272-1276
enum MethodEndpoint<S, E> {
None,
Route(Route<E>),
BoxedHandler(BoxedIntoRoute<S, E>),
}三个变体,代表三种状态:
None——这个方法没有注册 handler。请求进来时,如果匹配到这个方法但字段是 None,就会跳过,继续检查下一个方法或者走 fallback。None 是每个字段的初始值——MethodRouter::new() 中所有九个字段都被设为 None。
Route(Route<E>)——已经是一个完整的 tower::Service。当你用 get_service(svc) 或 .on_service(MethodFilter::GET, svc) 注册一个 service 时,它直接被包装成 Route<E> 存入字段。Route<E> 本身是对 tower::Service + Clone 的包装,已经在构造期完成了从 T: Service 到可调用服务的转换。Route 内部使用 Arc 来共享底层的 service 实例,所以 clone 操作的成本只是一次引用计数增加。
BoxedHandler(BoxedIntoRoute<S, E>)——一个"还没准备好"的 handler。当你用 get(handler) 或 .post(handler) 注册一个 handler 函数时,它被包装成 BoxedIntoRoute。为什么不能直接转成 Route?因为 handler 函数可能需要 State<S>——而 state 在注册时还不存在,要等到 with_state(state) 调用时才能提供。BoxedIntoRoute 本质上是一个类型擦除的闭包——它把"给定 state 后如何把 handler 转成 Route"的知识装箱保存起来,等到 state 到位再执行转换。
这三种状态的区分不是设计上的冗余,而是生命周期差异的忠实反映。handler 函数在定义时不知道 state,service 在定义时已经自给自足。如果强行把 handler 也立刻转成 Route,就必须在注册时提供一个 state——但 Axum 的 API 设计允许你在 Router 构建完成后的任意时刻才调用 with_state,所以"延迟转换"是唯一的选择。
看 with_state 方法(routing/method_routing.rs:820-834),这个类型转换的逻辑一目了然:
rust
// routing/method_routing.rs:820-834
pub fn with_state<S2>(self, state: S) -> MethodRouter<S2, E> {
MethodRouter {
get: self.get.with_state(&state),
head: self.head.with_state(&state),
delete: self.delete.with_state(&state),
options: self.options.with_state(&state),
patch: self.patch.with_state(&state),
post: self.post.with_state(&state),
put: self.put.with_state(&state),
trace: self.trace.with_state(&state),
connect: self.connect.with_state(&state),
allow_header: self.allow_header,
fallback: self.fallback.with_state(state),
}
}而 MethodEndpoint::with_state 的实现在 1304-1310 行:
rust
// routing/method_routing.rs:1304-1310
fn with_state<S2>(self, state: &S) -> MethodEndpoint<S2, E> {
match self {
Self::None => MethodEndpoint::None,
Self::Route(route) => MethodEndpoint::Route(route),
Self::BoxedHandler(handler) => MethodEndpoint::Route(handler.into_route(state.clone())),
}
}关键在第三行:BoxedHandler 在获得 state 之后变成了 Route。handler.into_route(state) 把"等待 state 的 handler 函数"转换成了"已经拿到 state 的 tower Service"。转换完成后,状态类型从 S 变成了 S2——对于不需要 state 的 handler,S2 就是 ()。
注意 allow_header 字段在 with_state 中原样传递——它不依赖 state,因为 Allow 头的内容在注册 handler 时就已经确定了。这是一种"提前计算"的优化:每次注册方法时增量更新 AllowHeader,而不是在每次请求到来时遍历所有字段重新计算。
这就是 MethodEndpoint 三个变体存在的全部理由:None 表示空,Route 表示就绪,BoxedHandler 表示等待 state。一旦注入 state,所有 BoxedHandler 都会变成 Route,dispatch 时只需要处理两种情况。MethodEndpoint::map 方法(1290-1303 行)进一步证明了这一点——layer 应用时,None 保持不变,Route 和 BoxedHandler 各自通过不同路径应用中间件,但最终都还是这两种有效状态。
顶层 API:函数工厂与链式调用
注册 handler 有两种入口:顶层函数和链式方法。
顶层函数:get()、post()、on()、any()
routing/method_routing.rs 用两个宏批量生成了这些函数。top_level_handler_fn! 宏(105-174 行)生成 handler 版本,top_level_service_fn! 宏(27-103 行)生成 service 版本。这种宏驱动的代码生成是 Axum 源码里反复出现的模式——九个 HTTP 方法的处理逻辑几乎相同,只有方法名和 MethodFilter 常量不同,用宏消除重复是自然的选择。
最简单的入口是 get(handler),它的展开结果如下(165-173 行):
rust
// 宏展开后的等价代码
pub fn get<H, T, S>(handler: H) -> MethodRouter<S, Infallible>
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
on(MethodFilter::GET, handler)
}所有顶层函数最终都委托给 on(filter, handler)(466-473 行),而 on 又委托给 MethodRouter::new().on(filter, handler)。这种"顶层函数 = 构造空 MethodRouter + 注册一个方法"的设计使得每个顶层函数的实现都只有一行——真正的逻辑全部在 MethodRouter::on 和 on_endpoint 里。
更灵活的入口是 on(MethodFilter, handler)。它允许你用一个 MethodFilter 同时注册多个方法——比如 on(MethodFilter::GET.or(MethodFilter::HEAD), handler)。不过实际上你更可能写 get(handler).head(handler2),因为大多数场景下 GET 和 HEAD 走不同的逻辑。on 的真正用途是配合自定义的 MethodFilter 组合——比如注册一组 WebDAV 扩展方法。
最特殊的是 any(handler)(508-515 行):
rust
// routing/method_routing.rs:508-515
pub fn any<H, T, S>(handler: H) -> MethodRouter<S, Infallible>
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
MethodRouter::new().fallback(handler).skip_allow_header()
}注意 any() 不往任何方法字段里放 handler——它直接设置 fallback。这意味着所有方法都会走 fallback handler。同时它调用 skip_allow_header() 把 AllowHeader 设为 Skip,因为"接受所有方法"的语义下不需要 Allow 头——列出"GET,HEAD,POST,PUT,DELETE,PATCH,OPTIONS,TRACE,CONNECT"既冗余又没有信息量,客户端看到 any() 路由时不需要知道支持哪些方法,因为答案是"全部"。
any() 之后还能链式调用其他方法,比如 any(handler).post(other)。这时候 POST 请求会走 post 字段的 other(因为 call_with_state 先检查方法字段再走 fallback),其他请求走 fallback。这符合直觉:显式注册优先于兜底。这种"链式覆盖"的设计让 any() 可以作为一个基础,然后在特定方法上做精细化处理。
链式方法:.get().post().put()
chained_handler_fn! 宏(263-333 行)为 MethodRouter 生成了九个链式方法。每个方法都调用 self.on(MethodFilter::$method, handler):
rust
// 宏展开后的等价代码
pub fn post<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
S: Send + Sync + 'static,
{
self.on(MethodFilter::POST, handler)
}链式调用的核心是 on_endpoint 方法(870-990 行)。它做了两件事:设置 endpoint 和维护 AllowHeader。on_endpoint 的实现通过内部函数 set_endpoint(873-897 行)来减少编译器生成的 IR 中间表示——这是一个编译期优化注释里提到的原因(871 行:// written as a separate function to generate less IR)。如果同一个方法被注册两次,它会 panic:
rust
// routing/method_routing.rs:886-891
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();这就是 get(ok).get(ok) 会在运行时 panic 的原因——overlap 检测发生在 on_endpoint 里。注意这是运行时 panic,不是编译期错误。Rust 的类型系统无法在编译期区分"注册过 GET 的 MethodRouter"和"没注册过 GET 的 MethodRouter"——除非引入更复杂的类型状态(比如让 MethodRouter<HasGet> 和 MethodRouter<NoGet> 是不同类型),但 Axum 选择了简单性。相比之下,一些更激进的类型状态设计(比如 typed-builder crate)会在编译期阻止重复设置,但代价是显著增加类型复杂度和编译时间。Axum 的选择是务实的:运行时 panic 足够早地暴露问题(在路由初始化阶段而非请求处理阶段),而不会把 API 变得难以使用。
测试用例(1607-1613 行)直接验证了 overlap panic 的行为:
rust
// routing/method_routing.rs:1607-1613 (test)
#[should_panic(
expected = "Overlapping method route. Cannot add two method routes that both handle `GET`"
)]
async fn handler_overlaps() {
let _: MethodRouter<()> = get(ok).get(ok);
}GET 与 HEAD:不重叠的特殊规则
在 on_endpoint 的 set_endpoint 调用中,有一个细节值得注意(899-907 行):
rust
// routing/method_routing.rs:899-907
set_endpoint(
"GET",
&mut self.get,
endpoint,
filter,
MethodFilter::GET,
&mut self.allow_header,
&["GET", "HEAD"], // <-- 注意这里
);当注册一个 GET handler 时,Allow 头里会同时添加 "GET" 和 "HEAD"。这是因为 RFC 9110 规定:如果服务器支持某个资源的 GET,它必须也支持 HEAD(除非显式拒绝)。Axum 遵守了这个规范——注册 GET handler 意味着 HEAD 也隐式可用,所以 Allow 头里必须出现 "HEAD"。
对比之下,注册 HEAD handler 时只添加 "HEAD"(909-917 行):
rust
// routing/method_routing.rs:909-917
set_endpoint(
"HEAD",
&mut self.head,
endpoint,
filter,
MethodFilter::HEAD,
&mut self.allow_header,
&["HEAD"], // 只添加 HEAD
);这是不对称的,但语义上是正确的:注册 HEAD 不意味着 GET 也隐式可用,但注册 GET 意味着 HEAD 隐式可用。HTTP 协议的约束是单向的——GET 暗示 HEAD,但 HEAD 不暗示 GET。
get(ok).head(ok) 不会 panic——HEAD 和 GET 的字段是独立的,注册顺序不影响。你可以显式注册一个不同的 HEAD handler 来覆盖隐式的 GET-for-HEAD 行为。这种设计给了一个有用的优化空间:HEAD 请求通常只需要返回 header 不需要查询数据库,你可以给 HEAD 注册一个轻量 handler,而让 GET 走完整业务逻辑。
MethodFilter:位运算驱动的过滤器
MethodFilter 在 routing/method_filter.rs 中定义,只有 165 行,但它是 MethodRouter 运转的基础设施。
rust
// routing/method_filter.rs:8-9
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct MethodFilter(u16);一个 u16,9 个位,每个位代表一个 HTTP 方法:
rust
// routing/method_filter.rs:29-45
pub const CONNECT: Self = Self::from_bits(0b0_0000_0001); // bit 0
pub const DELETE: Self = Self::from_bits(0b0_0000_0010); // bit 1
pub const GET: Self = Self::from_bits(0b0_0000_0100); // bit 2
pub const HEAD: Self = Self::from_bits(0b0_0000_1000); // bit 3
pub const OPTIONS:Self = Self::from_bits(0b0_0001_0000); // bit 4
pub const PATCH: Self = Self::from_bits(0b0_0010_0000); // bit 5
pub const POST: Self = Self::from_bits(0b0_0100_0000); // bit 6
pub const PUT: Self = Self::from_bits(0b0_1000_0000); // bit 7
pub const TRACE: Self = Self::from_bits(0b1_0000_0000); // bit 8为什么用位掩码而不是枚举?因为 MethodFilter 的核心操作是组合——"这个路由支持 GET 和 POST"需要用一个值同时表示两个方法。位掩码天然支持 OR 组合:
rust
// routing/method_filter.rs:62-64
pub const fn or(self, other: Self) -> Self {
Self(self.0 | other.0)
}以及包含检查:
rust
// routing/method_filter.rs:56-58
pub(crate) const fn contains(self, other: Self) -> bool {
self.bits() & other.bits() == other.bits()
}MethodFilter::GET.or(MethodFilter::POST) 的结果是 0b0_0100_0100,contains(GET) 为真,contains(DELETE) 为假。这种位运算在现代 CPU 上只需要一条指令,对编译器来说还可以被内联和常量折叠——如果你的 MethodFilter 是编译期常量,contains 的结果在编译期就能算出来。
TryFrom<Method> 的实现(88-105 行)把 http::Method 转换成 MethodFilter。对于标准的 9 个方法,转换成功;对于自定义方法(如 "PROPFIND"),转换失败并返回 NoMatchingMethodFilter 错误。这意味着 MethodFilter 只能表示标准方法——自定义方法无法通过 on() 注册,必须使用 any() 或 fallback() 来处理。这是一个有意的限制:如果开放自定义方法,MethodRouter 的九个命名字段设计就不够用了,需要退回到 HashMap 方案。
method_filter() 方法(673-709 行)用这个位掩码来汇总 MethodRouter 当前注册了哪些方法:
rust
// routing/method_routing.rs:692-706
let filter = [
(get, MethodFilter::GET),
(head, MethodFilter::HEAD),
(delete, MethodFilter::DELETE),
(options, MethodFilter::OPTIONS),
(patch, MethodFilter::PATCH),
(post, MethodFilter::POST),
(put, MethodFilter::PUT),
(trace, MethodFilter::TRACE),
(connect, MethodFilter::CONNECT),
]
.into_iter()
.filter_map(|(ep, f)| ep.is_some().then_some(f))
.reduce(MethodFilter::or)
.expect("can't create a MethodRouter with all-default handlers");遍历所有方法字段,is_some() 的转成对应的 MethodFilter,然后 OR 到一起。如果设置了自定义 fallback 或者用了 any(),这个方法返回 None——因为 fallback 意味着"所有方法都可能被处理",位掩码失去了精确描述的意义。
位掩码的选择在 Rust 生态里非常普遍:tokio::io::Interest 用了同样的模式来表示读写兴趣的组合,hyper 的 Proto 也是位掩码来表示协议版本。这是一种在"表达组合语义"和"保持零成本"之间的经典平衡。
请求分发:call_with_state 全景
call_with_state 是 MethodRouter 最核心的方法,定义在 1167-1222 行。当你写 Router::new().route("/users", get(list).post(create)) 并且一个请求到达 /users 时,Router 先完成路径匹配,然后把请求交给对应路径的 MethodRouter::call_with_state。
rust
// routing/method_routing.rs:1167-1222
pub(crate) fn call_with_state(&self, req: Request, state: S) -> RouteFuture<E> {
macro_rules! call {
($req:expr, $method_variant:ident, $svc:expr) => {
if *req.method() == Method::$method_variant {
match $svc {
MethodEndpoint::None => {}
MethodEndpoint::Route(route) => {
return route.clone().oneshot_inner_owned($req);
}
MethodEndpoint::BoxedHandler(handler) => {
let route = handler.clone().into_route(state);
return route.oneshot_inner_owned($req);
}
}
}
};
}
let Self {
get, head, delete, options, patch,
post, put, trace, connect,
fallback, allow_header,
} = self;
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!:匹配与分发
call! 宏做了三件事:比较请求方法、匹配 endpoint 变体、调用 service。
关键在 route.clone().oneshot_inner_owned($req)——每次调用都 clone 了一份 Route。这是 Axum 的一个核心设计决策:Service 的调用不是 &mut self,而是 clone 后 owned 调用。在 tower::Service 的定义里,call(&mut self, req) 需要 &mut self,这意味着同一个 Service 实例同一时刻只能处理一个请求。但 HTTP/2 允许在单个 TCP 连接上并发多个请求。解决方案是:每次调用前 clone 一份 Service,让每个请求拥有独立的 Service 实例。
这正是《Hyper 与 Tower》第 13 章讨论过的主题——为什么 hyper 的 Service 用 &self 而不是 &mut self。核心原因就是并发:如果 Service 只需要 &self,就不需要 clone;但 tower::Service 的 trait 签名要求 &mut self,所以 clone 成了唯一的选择。Axum 全线采用 clone-based dispatch,所有 Route 和 MethodRouter 都实现了 Clone,clone 的成本通常是 Arc 引用计数的增加——对于大多数场景,这比加锁或者队列化要高效得多。
oneshot_inner_owned 的名字也值得解读。"oneshot" 表示这个 service 只会被调用一次——调用完毕就丢弃。"inner" 说明它绕过了外层的 poll_ready 检查(因为 Axum 的 service 总是 ready)。"owned" 表示它消费了 self(通过 clone 获得),而不是用 &mut self 调用。这三个词合在一起精确描述了"clone 一次、调用一次、丢弃"的 dispatch 模式。
BoxedHandler 分支多了一步:handler.clone().into_route(state)。这是"注入 state 并转为 Route"的延迟操作——只有到真正需要调用时才执行。注意这个分支每次调用都会执行 into_route——这意味着同一个 BoxedHandler 在 with_state 之后已经变成了 Route,不会再走这个分支。这个分支只在"状态类型还是 S 而非 ()"的 MethodRouter 上存在——即还没调用 with_state 的 MethodRouter。在正常的 Router 使用流程中,with_state 在服务启动前被调用一次,之后所有请求走的都是 Route 分支。
分发顺序:HEAD 先于 GET
注意分发顺序(1204-1213 行):
rust
call!(req, HEAD, head); // 先查专用 HEAD handler
call!(req, HEAD, get); // 再查 GET handler 做 HEAD
call!(req, GET, get); // 然后才是 GET
call!(req, POST, post); // ... 其他方法HEAD 请求首先尝试 head 字段,如果没有注册专用 HEAD handler,再尝试 get 字段。这实现了 "GET 隐式处理 HEAD" 的语义——但专用 HEAD handler 优先级更高。
这就是为什么 get(ok).head(created) 中,HEAD 请求会走 created 而不是 ok。测试用例(1439-1443 行)直接验证了这个行为:
rust
// routing/method_routing.rs:1439-1443 (test)
async fn head_takes_precedence_over_get() {
let mut svc = MethodRouter::new().head(created).get(ok);
let (status, _, body) = call(Method::HEAD, &mut svc).await;
assert_eq!(status, StatusCode::CREATED);
assert!(body.is_empty());
}而如果只有 GET handler,HEAD 请求会走 GET handler 但 body 被自动清空(1431-1436 行):
rust
// routing/method_routing.rs:1431-1436 (test)
async fn get_accepts_head() {
let mut svc = MethodRouter::new().get(ok);
let (status, _, body) = call(Method::HEAD, &mut svc).await;
assert_eq!(status, StatusCode::OK);
assert!(body.is_empty());
}body 清空不是在 call_with_state 里做的——它发生在更底层的 hyper 响应处理中。当 hyper 检测到请求方法是 HEAD 时,会自动剥除响应体。这是协议栈的正确位置来处理这件事,因为只有 hyper 知道底层传输是 HTTP/1.1 还是 HTTP/2,它们的 body 帧格式不同。Axum 不需要在 MethodRouter 层做 body 剥除,hyper 已经处理了。
分发顺序中还有一个微妙之处:HEAD 请求的 call! 宏调用出现了两次——一次匹配 head 字段,一次匹配 get 字段。这是因为宏的第一个参数 $method_variant 是固定的 HEAD,第二个参数 $svc 分别是 head 和 get。同一个请求方法可以检查多个字段,这是 call! 宏的设计意图——它不是"一个方法只查一个字段"的简单映射,而是一个灵活的"方法→字段"查找序列。除了 HEAD 的特殊处理外,其他方法严格遵循一对一映射:POST 只查 post 字段,DELETE 只查 delete 字段,以此类推。这种"大部分一对一、HEAD 特殊"的模式恰好反映了 HTTP 协议的实际情况——HEAD 是唯一一个"可以由另一个方法的 handler 隐式处理"的方法。
全部不匹配:走 fallback
如果请求方法不匹配任何已注册的 endpoint,控制流到达 1215 行:
rust
let future = fallback.clone().call_with_state(req, state);默认的 fallback 在 MethodRouter::new() 中设置(799-817 行):
rust
// routing/method_routing.rs:799-817
pub fn new() -> Self {
let fallback = Route::new(service_fn(|_: Request| async {
Ok(StatusCode::METHOD_NOT_ALLOWED)
}));
Self {
get: MethodEndpoint::None,
head: MethodEndpoint::None,
delete: MethodEndpoint::None,
options: MethodEndpoint::None,
patch: MethodEndpoint::None,
post: MethodEndpoint::None,
put: MethodEndpoint::None,
trace: MethodEndpoint::None,
connect: MethodEndpoint::None,
allow_header: AllowHeader::None,
fallback: Fallback::Default(fallback),
}
}默认 fallback 返回 405 Method Not Allowed。这是正确的 HTTP 语义——路径存在但方法不被允许。注意 fallback 的实现直接忽略了请求内容(|_: Request|),只返回状态码——默认的 405 响应没有 body,也没有自定义 header。
你可以用 .fallback(handler) 替换默认 fallback。设置自定义 fallback 后,所有不匹配的方法都会走这个 handler。这也是 any(handler) 的实现方式——它把 handler 设为 fallback,所有方法都不匹配 → 走 fallback → 等于所有方法都走这个 handler。
Fallback:三层兜底
Fallback<S, E> 定义在 routing/mod.rs:710-714:
rust
// routing/mod.rs:710-714
enum Fallback<S, E = Infallible> {
Default(Route<E>),
Service(Route<E>),
BoxedHandler(BoxedIntoRoute<S, E>),
}三个变体的语义不同:
Default:构造时自动创建的 405 fallback。在merge时,如果一方是Default,另一方不是,就采用非 Default 那一方。如果两方都不是 Default(即都设了自定义 fallback),合并失败——两个 MethodRouter 不能同时拥有自定义 fallback。Service:通过fallback_service(svc)设置的 tower Service。行为和Default一样,但来源不同,在merge时不会被Default覆盖。BoxedHandler:通过fallback(handler)设置的 handler 函数。和MethodEndpoint::BoxedHandler一样,需要 state 才能转换为 Route。
Fallback::call_with_state 的实现(751-759 行)清晰地展示了三种变体的调用路径:
rust
// routing/mod.rs:751-759
fn call_with_state(self, req: Request, state: S) -> RouteFuture<E> {
match self {
Self::Default(route) | Self::Service(route) => route.oneshot_inner_owned(req),
Self::BoxedHandler(handler) => {
let route = handler.into_route(state);
route.oneshot_inner_owned(req)
}
}
}Default 和 Service 走同一个分支——它们都是 Route<E>,区别只在 merge 时的行为。BoxedHandler 需要先用 state 转成 Route 再调用。Fallback::with_state(743-748 行)的实现也遵循同样的模式:Default 和 Service 保持不变(它们已经是 Route,不依赖 state),BoxedHandler 注入 state 后变成 Service 变体——因为 into_route 返回一个 Route<E>,而 Fallback::Service 正好包装 Route<E>。这个转换使得 with_state 之后的 Fallback 只有两个变体存在(Default 和 Service),BoxedHandler 被完全消除,和 MethodEndpoint::with_state 中 BoxedHandler 变成 Route 的逻辑完全对称。
Fallback::merge 的逻辑在 720-727 行:
rust
// routing/mod.rs:720-727
fn merge(self, other: Self) -> Option<Self> {
match (self, other) {
(Self::Default(_), pick) | (pick, Self::Default(_)) => Some(pick),
_ => None,
}
}None 表示合并失败。对应的错误消息在 1129 行:
rust
// routing/method_routing.rs:1129
self.fallback = self.fallback
.merge(other.fallback)
.ok_or("Cannot merge two `MethodRouter`s that both have a fallback")?;这个设计是合理的:两个 MethodRouter 各自定义了 fallback,合并时无法决定保留哪个。但 Default 不是"真正的" fallback——它只是 405 的占位符——所以可以被任何非 Default 的 fallback 覆盖。is_default() 方法(761-763 行)用来判断 fallback 是否是默认的——method_filter() 方法在 fallback 不是 Default 时返回 None。
AllowHeader:405 响应的灵魂
405 响应如果不带 Allow 头,客户端就不知道这个路径支持哪些方法。RFC 9110 第 15.5.6 节要求 405 响应必须包含 Allow 头。Axum 的 AllowHeader 就是为此而生。
rust
// routing/method_routing.rs:561-569
#[derive(Clone, Debug)]
enum AllowHeader {
/// 还没有构建任何 Allow 值,默认状态
None,
/// 不设置 Allow 头,用于 any() 或 any_service()
Skip,
/// 当前 Allow 头的值
Bytes(BytesMut),
}三个状态的语义:
None:初始状态。MethodRouter 刚构造时,allow_header是None。如果没有任何方法被注册,最终 405 响应的Allow头为空字符串。这个"空字符串"和"没有 Allow 头"是不同的——None依然会在响应中写入一个空的Allow:头,而Skip则完全不写入。Skip:跳过Allow头。当调用any()或any_service()时设置。因为这些函数接受所有方法,生成Allow: GET,HEAD,POST,PUT,DELETE,PATCH,OPTIONS,TRACE,CONNECT既冗余又不准确(你注册的 handler 可能根本不支持某些方法的语义),所以干脆不设。Bytes(BytesMut):已构建的Allow头值。使用BytesMut而不是String是为了避免 UTF-8 验证开销——HTTP 头的值都是 ASCII,不需要 UTF-8 检查。BytesMut的另一个好处是支持extend_from_slice进行零拷贝追加——每次注册新方法时只需要在已有字节后面追加逗号和方法名。
构建过程:每次注册方法时追加
append_allow_header 函数(1225-1243 行)负责在注册新方法时追加 Allow 值:
rust
// routing/method_routing.rs:1225-1243
fn append_allow_header(allow_header: &mut AllowHeader, method: &'static str) {
match allow_header {
AllowHeader::None => {
*allow_header = AllowHeader::Bytes(BytesMut::from(method));
}
AllowHeader::Skip => {}
AllowHeader::Bytes(allow_header) => {
if let Ok(s) = std::str::from_utf8(allow_header) {
if !s.contains(method) {
allow_header.extend_from_slice(b",");
allow_header.extend_from_slice(method.as_bytes());
}
}
}
}
}首次注册方法时,None 变成 BytesMut::from("GET")。第二次注册,追加 ",HEAD"(如果注册了 GET,HEAD 会被自动添加,因为 set_endpoint 中 GET 的 methods 参数是 &["GET", "HEAD"])。
去重逻辑在 !s.contains(method) ——防止同一个方法名被添加两次。注意这里用的是字符串 contains 而不是精确匹配,因为 HTTP 方法名都是全大写的唯一子串,contains 足够准确。"GET" 不会出现在 "TARGET" 里,"POST" 不会出现在 "POSTER" 里——这些是 HTTP 标准方法名的固有特性。
Skip 状态下 append_allow_header 什么都不做——一旦进入 Skip 状态,任何追加操作都被忽略。这是因为 Skip 代表"不需要 Allow 头",这个决定是终局的——即使后来链式调用了 .get(handler),AllowHeader 仍然保持 Skip。这符合 any() 的语义:当你用 any() 声明"接受所有方法"后,即使又注册了具体方法,Allow 头的存在也不会增加信息量。
合并时的 AllowHeader
当两个 MethodRouter 合并时,AllowHeader 也要合并(1131 行):
rust
self.allow_header = self.allow_header.merge(other.allow_header);merge 方法的实现在 572-584 行:
rust
// routing/method_routing.rs:572-584
fn merge(self, other: Self) -> Self {
match (self, other) {
(Self::Skip, _) | (_, Self::Skip) => Self::Skip,
(Self::None, Self::None) => Self::None,
(Self::None, Self::Bytes(pick)) | (Self::Bytes(pick), Self::None) => Self::Bytes(pick),
(Self::Bytes(mut a), Self::Bytes(b)) => {
a.extend_from_slice(b",");
a.extend_from_slice(&b);
Self::Bytes(a)
}
}
}规则很清晰:Skip 具有最高优先级(任意一方是 Skip,结果就是 Skip);None 被有值的一方覆盖;两个 Bytes 用逗号连接。
测试用例(1538-1546 行)验证了合并行为:
rust
// routing/method_routing.rs:1538-1546 (test)
async fn allow_header_when_merging() {
let a = put(ok).patch(ok);
let b = get(ok).head(ok);
let mut svc = a.merge(b);
let (status, headers, _) = call(Method::DELETE, &mut svc).await;
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(headers[ALLOW], "PUT,PATCH,GET,HEAD");
}合并后 Allow 头的值是 "PUT,PATCH,GET,HEAD"——前半部分来自 a,后半部分来自 b,用逗号连接。注意这里没有去重——因为 a 和 b 的方法集合不重叠(merge 的前置条件保证了这一点),所以合并后的 Allow 值自然不会有重复方法名。
在 405 响应中写入 Allow 头
回到 call_with_state 的结尾(1217-1221 行):
rust
match allow_header {
AllowHeader::None => future.allow_header(Bytes::new()),
AllowHeader::Skip => future,
AllowHeader::Bytes(allow_header) => {
future.allow_header(allow_header.clone().freeze())
}
}fallback.call_with_state 返回一个 RouteFuture<E>,然后根据 AllowHeader 的状态决定是否给它附加 Allow 头。AllowHeader::None 时设置一个空的 Allow 头(Bytes::new()),AllowHeader::Skip 时什么也不做,AllowHeader::Bytes 时把预构建的值写入。
RouteFuture::allow_header 是一个在 future 被轮询时写入响应头的机制——它不在 call_with_state 里直接写响应,而是标记"等响应生成后,往 header 里注入 Allow"。这种延迟写入是必要的,因为 fallback handler 的执行是异步的——你不知道它什么时候返回响应,只能在 future 被轮询时拦截。RouteFuture 内部存储了 Option<Bytes> 类型的 allow header 值,在生成响应时检查这个值,如果存在就插入到响应 header 中。
allow_header.clone().freeze() 把 BytesMut 转换成 Bytes——前者是可变的,后者是不可变的。freeze 操作不需要拷贝数据,只是改变了元数据的标记。
测试用例(1514-1519 行)完整验证了 Allow 头的行为:
rust
// routing/method_routing.rs:1514-1519 (test)
async fn sets_allow_header() {
let mut svc = MethodRouter::new().put(ok).patch(ok);
let (status, headers, _) = call(Method::GET, &mut svc).await;
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(headers[ALLOW], "PUT,PATCH");
}GET 请求打到一个只注册了 PUT 和 PATCH 的 MethodRouter,得到 405 + Allow: PUT,PATCH。客户端据此知道,这个路径支持 PUT 和 PATCH。
GET 隐式处理 HEAD:RFC 9110 的实现
前文多次提到"GET 隐式处理 HEAD",这里做一个完整的梳理。
RFC 9110 第 9.1 节的原话是:
The HEAD method is identical to GET except that the server MUST NOT send a message body in the response.
Axum 实现这个规范的方式分为两层:
路由注册层:当注册 GET handler 时,on_endpoint 中的 set_endpoint 调用(899-907 行)把 &["GET", "HEAD"] 传给 append_allow_header。这意味着 Allow 头中 HEAD 和 GET 一起出现——告诉客户端"这个路径支持 HEAD"。
请求分发层:call_with_state 中的分发顺序是 HEAD → head字段 → HEAD → get字段 → GET → get字段(1204-1206 行)。HEAD 请求先尝试专用 HEAD handler,如果没有,就走 GET handler。GET handler 返回完整的响应(包括 body),但 hyper 底层会自动剥除 HEAD 响应的 body。
不冲突的设计:get(ok).head(ok) 不会 panic,因为 GET 和 HEAD 的字段是独立的。set_endpoint 只在同一个字段上重复注册时才 panic。这允许你给 HEAD 注册一个轻量级的 handler(比如只返回 header 不查询数据库),同时让 GET 走完整的业务逻辑。
Allow 头的正确性:如果你只注册了 GET handler,一个 DELETE 请求返回的 405 响应中 Allow 头应该包含 "GET,HEAD"。Axum 在注册 GET 时就把 "HEAD" 加入了 AllowHeader,所以这是自动满足的。测试用例(1522-1527 行)验证了这个行为:
rust
// routing/method_routing.rs:1522-1527 (test)
async fn sets_allow_header_get_head() {
let mut svc = MethodRouter::new().get(ok).head(ok);
let (status, headers, _) = call(Method::PUT, &mut svc).await;
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(headers[ALLOW], "GET,HEAD");
}注意这里 .get(ok).head(ok) 的 Allow 头是 "GET,HEAD" 而不是 "HEAD,GET"——因为注册 GET 时先写入了 "GET,HEAD"(set_endpoint 的 methods 参数是 &["GET", "HEAD"]),然后注册 HEAD 时 append_allow_header 发现 "HEAD" 已经在字符串中,跳过了追加。Allow 头中方法名的顺序不影响语义——RFC 9110 没有规定 Allow 头中方法名的排列顺序。
下面这张图展示了 GET 与 HEAD 交互的完整状态机:
merge:两个 MethodRouter 的组合
merge 方法(1136-1144 行)是 MethodRouter 的另一个核心操作。它的典型用例是:不同的模块各自定义一个 MethodRouter,最后在 Router 层面合并。
rust
// routing/method_routing.rs:1136-1144
pub fn merge(self, other: Self) -> Self {
match self.merge_for_path(None, other) {
Ok(t) => t,
Err(e) => panic!("{e}"),
}
}merge_for_path(1084-1134 行)做了三件事:
合并方法字段:对每个方法调用 merge_inner(1090-1113 行)。规则是:如果一方是 None,取非 None 的一方;如果双方都有值,报 overlap 错误。
rust
// routing/method_routing.rs:1096-1113
fn merge_inner<S, E>(
path: Option<&str>,
name: &str,
first: MethodEndpoint<S, E>,
second: MethodEndpoint<S, E>,
) -> Result<MethodEndpoint<S, E>, Cow<'static, str>> {
match (first, second) {
(MethodEndpoint::None, MethodEndpoint::None) => Ok(MethodEndpoint::None),
(pick, MethodEndpoint::None) | (MethodEndpoint::None, pick) => Ok(pick),
_ => {
if let Some(path) = path {
Err(format!(
"Overlapping method route. Handler for `{name} {path}` already exists"
).into())
} else {
Err(format!(
"Overlapping method route. Cannot merge two method routes that both \
define `{name}`"
).into())
}
}
}
}注意 merge_inner 在返回 overlap 错误时区分了两种场景:有路径信息时(通过 merge_for_path 的 path 参数),错误消息会包含具体路径(如 "Handler for GET /users already exists");没有路径信息时,错误消息只指出方法冲突。这让 Router::route 在检测到路径级别的方法冲突时能给出更精确的错误信息。
合并 fallback:如前所述,两个非 Default 的 fallback 不能合并。
合并 AllowHeader:如前所述,AllowHeader::merge 处理合并。
merge_for_path 的 11 次 merge_inner 调用(1116-1124 行)逐一处理每个方法字段。虽然代码看起来是重复的,但这种展开式的写法保证了每个方法都被处理——和 call_with_state 中的九次 call! 宏调用一样,用代码的冗长换取完备性的保证。
测试用例(1447-1458 行)验证了正常合并的行为:
rust
// routing/method_routing.rs:1447-1458 (test)
async fn merge() {
let mut svc = get(ok).merge(post(ok)).merge(connect(ok));
let (status, _, _) = call(Method::GET, &mut svc).await;
assert_eq!(status, StatusCode::OK);
let (status, _, _) = call(Method::POST, &mut svc).await;
assert_eq!(status, StatusCode::OK);
let (status, _, _) = call(Method::CONNECT, &mut svc).await;
assert_eq!(status, StatusCode::OK);
}三次 merge 把 GET、POST、CONNECT 三个方法合并到一个 MethodRouter 中,每种方法都能正确匹配。
MethodRouter 作为 Handler:嵌套的钥匙
MethodRouter 实现了 Handler<(), S> trait(1355-1364 行):
rust
// routing/method_routing.rs:1355-1364
impl<S> Handler<(), S> for MethodRouter<S>
where
S: Clone + 'static,
{
type Future = InfallibleRouteFuture;
fn call(self, req: Request, state: S) -> Self::Future {
InfallibleRouteFuture::new(self.call_with_state(req, state))
}
}这个 impl 是 MethodRouter 能嵌套的根本原因。当你写:
rust
Router::new().route("/api", any(api_routes))any(api_routes) 把 api_routes(一个 MethodRouter)设为 fallback,而 api_routes 能成为 fallback 的参数,正是因为它实现了 Handler。这里 Handler trait 的 marker 类型参数 T 是 (),意味着 MethodRouter 不需要任何额外的提取器——它自己就是完整的分发器。
MethodRouter 同时也实现了 Service<Request<B>>(1333-1352 行),但只限 S = () 的状态——即不需要 state 的 MethodRouter。这是合理的:tower::Service 没有"注入 state"的概念,所以只有不需要 state 的 MethodRouter 才能直接作为 Service 使用。需要 state 的 MethodRouter 必须先调用 with_state,然后才能作为 Service。
InfallibleRouteFuture 的使用也值得注意。Handler::Future 的关联类型是 Future<Output = Result<Response, E>>,而 InfallibleRouteFuture 把 E 固定为 Infallible。这意味着 MethodRouter 作为 Handler 永远不会返回错误——它的错误处理已经在内部完成了(405 是正常响应,不是错误)。Infallible 的使用是 Axum 的标志性设计——当你看到一个 Infallible 在错误位置出现,就意味着"这个操作不可能失败"。
MethodRouter 作为独立服务器
MethodRouter 的 Handler 和 Service 实现带来了一个意想不到的用途:你可以跳过 Router,直接用 MethodRouter 启动一个 HTTP 服务。into_make_service 方法(754-756 行)为此提供了支持:
rust
// routing/method_routing.rs:754-756
pub fn into_make_service(self) -> IntoMakeService<Self> {
IntoMakeService::new(self.with_state(()))
}这使得以下代码成为可能:
rust
let router = get(handler).post(handler);
let listener = TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, router.into_make_service()).await?;不需要 Router、不需要路径匹配——一个 MethodRouter 就是一个完整的服务。这在写简单的健康检查端点或者单路径 API 时很方便。into_make_service_with_connect_info 方法(788-790 行)在此基础上还支持获取客户端连接信息(如 IP 地址)。
layer 与 route_layer:两种中间件策略
MethodRouter 提供了两种添加中间件的方式:layer 和 route_layer。它们的区别在于中间件是否覆盖 fallback。
layer:全方法覆盖
layer 方法(1013-1040 行)对 MethodRouter 的所有端点应用中间件——包括 fallback:
rust
// routing/method_routing.rs:1027-1039
MethodRouter {
get: self.get.map(layer_fn.clone()),
head: self.head.map(layer_fn.clone()),
delete: self.delete.map(layer_fn.clone()),
options: self.options.map(layer_fn.clone()),
patch: self.patch.map(layer_fn.clone()),
post: self.post.map(layer_fn.clone()),
put: self.put.map(layer_fn.clone()),
trace: self.trace.map(layer_fn.clone()),
connect: self.connect.map(layer_fn.clone()),
fallback: self.fallback.map(layer_fn),
allow_header: self.allow_header,
}注意 layer_fn 被 clone 了九次——每个方法字段各一份。这是因为 layer_fn 是一个闭包,捕获了 layer 参数。clone 的成本取决于 layer 本身——大多数 tower layer 都是轻量的包装器,clone 成本很低。
如果一个请求方法不匹配任何已注册的 endpoint,走 fallback,但 fallback 也被 layer 包装了。这意味着 layer 添加的中间件对 405 响应也生效。layer 改变了 MethodRouter 的错误类型 E——因为中间件可能引入新的错误类型,所以返回值变成了 MethodRouter<S, NewError>。
测试用例(1462-1474 行)验证了这个行为:
rust
// routing/method_routing.rs:1462-1474 (test)
async fn layer() {
let mut svc = MethodRouter::new()
.get(|| async { std::future::pending::<()>().await })
.layer(ValidateRequestHeaderLayer::bearer("password"));
// method with route -> 401 Unauthorized
let (status, _, _) = call(Method::GET, &mut svc).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
// method without route -> also 401 (fallback is also wrapped)
let (status, _, _) = call(Method::DELETE, &mut svc).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
}GET 和 DELETE 都返回 401——中间件包裹了所有端点。
route_layer:仅已注册方法
route_layer 方法(1043-1082 行)只对已注册的 endpoint 应用中间件,不影响 fallback。它还会检查是否已注册了任何 endpoint(1053-1067 行),如果没有则 panic——对空路由添加中间件是一个无操作,很可能是 bug:
rust
// routing/method_routing.rs:1053-1067
if self.get.is_none()
&& self.head.is_none()
&& self.delete.is_none()
// ... 检查所有 9 个字段
{
panic!(
"Adding a route_layer before any routes is a no-op. \
Add the routes you want the layer to apply to first."
);
}测试用例(1477-1490 行)验证了这个行为:
rust
// routing/method_routing.rs:1477-1490 (test)
async fn route_layer() {
let mut svc = MethodRouter::new()
.get(|| async { std::future::pending::<()>().await })
.route_layer(ValidateRequestHeaderLayer::bearer("password"));
// method with route -> 401
let (status, _, _) = call(Method::GET, &mut svc).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
// method without route -> 405 (fallback NOT wrapped)
let (status, _, _) = call(Method::DELETE, &mut svc).await;
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
}GET 返回 401(中间件生效),DELETE 返回 405(fallback 未被包裹)。两种策略的选择取决于你的需求:认证中间件应该用 layer(确保 405 响应也需要认证——否则攻击者可以通过 405 响应探测 API 结构),限流中间件可能用 route_layer(只对实际处理请求的方法限流,避免对 405 响应浪费限流配额)。
route_layer 不改变错误类型 E——因为中间件的错误类型必须和原有的一致。这也是为什么 route_layer 返回 Self 而 layer 返回 MethodRouter<S, NewError>。
layer 对 Allow 头的影响
无论是 layer 还是 route_layer,allow_header 字段都不会被修改——它原样传递。这意味着中间件不会影响 Allow 头的内容。测试用例(1597-1605 行)验证了这一点:
rust
// routing/method_routing.rs:1597-1605 (test)
async fn allow_header_noop_middleware() {
let mut svc = MethodRouter::new()
.get(ok)
.layer(tower::layer::util::Identity::new());
let (status, headers, _) = call(Method::DELETE, &mut svc).await;
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(headers[ALLOW], "GET,HEAD");
}Identity layer 是一个无操作中间件,但即使是有实际效果的中间件也不会改变 Allow 头的值——因为 Allow 头的内容在注册 handler 时就已经确定,中间件只影响请求的处理过程,不影响"哪些方法被注册"这个事实。
请求分发全景图
把以上所有机制串在一起,一个请求从到达 MethodRouter 到最终返回响应的完整流程如下:
这个流程图揭示了一个重要的事实:MethodRouter 的分发是线性扫描而非哈希查找。最多需要检查 10 次(HEAD 先查 head 再查 get,加上其余 8 个方法)的方法比较才能确定走哪个 endpoint。对于大多数 API 路径,注册的方法不会超过 3-4 个,所以实际扫描次数更少。线性扫描在方法数量有限的情况下比哈希查找更快——没有哈希计算、没有冲突处理、缓存局部性更好。
Endpoint:Router 视角的 MethodRouter
在 routing/mod.rs:787-789,有一个 Endpoint<S> 枚举:
rust
// routing/mod.rs:787-789
enum Endpoint<S> {
MethodRouter(MethodRouter<S>),
Route(Route),
}这是 Router 内部存储路径端点的数据结构。当你写 .route("/users", get(list).post(create)) 时,Router 在 /users 路径下存储一个 Endpoint::MethodRouter(...)。当你写 .route("/health", any(handler)) 且 handler 不是 MethodRouter 类型时,Router 可能存储一个 Endpoint::Route(...)。
Endpoint::layer 方法(796-808 行)展示了两种变体的中间件应用差异:
rust
// routing/mod.rs:796-808
fn layer<L>(self, layer: L) -> Self
where
L: Layer<Route> + Clone + Send + Sync + 'static,
// ... bounds
{
match self {
Self::MethodRouter(method_router) => {
Self::MethodRouter(method_router.layer(layer))
}
Self::Route(route) => Self::Route(route.layer(layer)),
}
}MethodRouter 和单 Route 共享同一个 layer 接口——对 Router 来说,它们都是"路径端点",区别只在于内部是否有方法分发逻辑。Endpoint 的存在让 Router 的 layer 方法不需要关心端点的具体类型——统一调用 Endpoint::layer 就行。
#[allow(clippy::large_enum_variant)] 注解(786 行)暗示了 MethodRouter 变体比 Route 变体大得多——九个 MethodEndpoint 字段加 fallback 和 allow_header,确实比单个 Route 大不少。如果这个枚举被频繁复制,大小差异可能导致内存浪费。但在 Router 的使用模式中,Endpoint 通常被 Arc 包装共享,所以这个大小差异影响有限。
为什么是 1723 行
回到本章的开头——为什么 method_routing.rs 有 1723 行,是 axum 仓库里最长的文件?
原因不是某个单一函数特别复杂,而是九个方法的对称性需求导致了大量重复模式。每个方法需要:一个顶层函数(get())、一个顶层 service 函数(get_service())、一个链式方法(.get())、一个链式 service 方法(.get_service())。4 乘以 9 等于 36——即使大部分逻辑用宏生成,宏本身的定义、文档注释、示例代码也占据了大量篇幅。
再加上 call_with_state 中的十次 call! 宏调用、on_endpoint 中的九次 set_endpoint 调用、merge_for_path 中的九次 merge_inner 调用、with_state 中的九次字段转换、new 中的九个字段初始化、layer 中的九次 map 调用、Clone 实现中的九次字段 clone——每次"对九个方法做同一件事"都会产生一批代码。
宏减少了逻辑的重复,但没有减少代码的物理行数。这些代码的每一行都有存在的理由——它们保证了九个方法的行为完全对称,不会因为遗漏某个方法而导致 bug。如果把 MethodRouter 重构成基于 HashMap 的方案,代码行数会大幅减少,但代价是失去编译期完备性保证和零间接寻址的性能确定性。
1723 行,本质上是完备性的代价。
MethodRouter 的 Service 实现:无状态分支
MethodRouter<(), E> 实现了 tower::Service<Request<B>>(1333-1352 行),但只限 S = () 的情况。这个限制的来源是 Service::call(&mut self, req) 没有"注入 state"的接口——它只接受请求,不接受额外的状态参数。所以只有不需要 state 的 MethodRouter(即 S = ())才能直接作为 Service 使用。
rust
// routing/method_routing.rs:1333-1352
impl<B, E> Service<Request<B>> for MethodRouter<(), E>
where
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,
{
type Response = Response;
type Error = E;
type Future = RouteFuture<E>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let req = req.map(Body::new);
self.call_with_state(req, ())
}
}poll_ready 永远返回 Ready(Ok(()))——MethodRouter 总是准备好的,不需要背压。call 把请求体映射成 Body(Axum 的统一 body 类型),然后委托给 call_with_state(req, ()),传入空 state。
对于需要 state 的 MethodRouter,你必须先调用 with_state(state) 得到 MethodRouter<(), E>,然后才能作为 Service。with_state 的类型签名 pub fn with_state<S2>(self, state: S) -> MethodRouter<S2, E> 表明:原来的 S 被消耗,新的状态类型 S2 通常是你不需要关心的——因为 state 已经被注入到 handler 内部了。
这个"状态消耗"的模式和 Rust 生态里的类型状态模式一脉相承。Router<S> 通过 with_state 变成 Router<()>,MethodRouter<S> 通过 with_state 变成 MethodRouter<S2>——状态的提供者(S)被消耗,转变成不需要状态的版本。编译器通过类型检查保证你不会忘记提供 state——如果你试图把一个 MethodRouter<String, E> 直接当 Service 用,编译器会报错,因为你还没提供 String 类型的 state。
总结:MethodRouter 的设计哲学
MethodRouter 是一个"小而完整"的 HTTP 方法分发器。它在 1723 行代码里解决了以下问题:
- 方法分发:九个命名字段,直接映射九个 HTTP 方法,无间接寻址。
- GET 隐式 HEAD:分发顺序先 HEAD 后 GET,符合 RFC 9110 语义,Allow 头自动包含 HEAD。
- 405 + Allow 头:所有方法不匹配时走 fallback,默认返回 405 并附带
Allow头列出已注册方法。 - 状态延迟注入:
BoxedHandler延迟到with_state时才转为 Route,支持 State 模式。 - 合并与冲突检测:
merge逐一合并方法字段,overlap 时 panic,两个自定义 fallback 不能合并。 - 两种中间件策略:
layer覆盖所有端点(含 fallback),route_layer只覆盖已注册方法。 - Handler trait 实现:MethodRouter 可以作为 Handler 嵌套到其他路由中,也可以独立启动服务。
它的设计选择体现了 Axum 的一贯风格:用结构体的命名字段保证完备性,用 clone-based dispatch 支持并发,用 panic 检测 overlap 而非引入复杂的类型状态。这些选择在简洁性和正确性之间找到了平衡。每个请求的方法分发最多只需十次方法比较和一次 Route clone——这是 Axum 路由分发的第一道关卡,也是性能最敏感的路径之一。
理解了 MethodRouter 如何按方法分发请求之后,下一个问题是:当路径需要嵌套——/api/v1/users 这种多层级结构——Router 如何组织和合并这些 MethodRouter?第 4 章将揭开嵌套与合并的机制。