Skip to content

第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 个:

方法幂等安全bodyRFC
GETRFC 9110 §9.3.1
HEADRFC 9110 §9.3.2
POSTRFC 9110 §9.3.3
PUTRFC 9110 §9.3.4
DELETE可选RFC 9110 §9.3.5
PATCHRFC 5789
OPTIONSRFC 9110 §9.3.7
TRACERFC 9110 §9.3.8
CONNECTRFC 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 的访问每次都要走相同的查表逻辑。

结构体还有两个额外的字段:fallbackallow_headerfallback 处理"没有任何方法匹配"的情况,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 之后变成了 Routehandler.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 保持不变,RouteBoxedHandler 各自通过不同路径应用中间件,但最终都还是这两种有效状态。

顶层 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::onon_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 和维护 AllowHeaderon_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_endpointset_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:位运算驱动的过滤器

MethodFilterrouting/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_0100contains(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——这意味着同一个 BoxedHandlerwith_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 分别是 headget。同一个请求方法可以检查多个字段,这是 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)
        }
    }
}

DefaultService 走同一个分支——它们都是 Route<E>,区别只在 merge 时的行为。BoxedHandler 需要先用 state 转成 Route 再调用。Fallback::with_state(743-748 行)的实现也遵循同样的模式:DefaultService 保持不变(它们已经是 Route,不依赖 state),BoxedHandler 注入 state 后变成 Service 变体——因为 into_route 返回一个 Route<E>,而 Fallback::Service 正好包装 Route<E>。这个转换使得 with_state 之后的 Fallback 只有两个变体存在(DefaultService),BoxedHandler 被完全消除,和 MethodEndpoint::with_stateBoxedHandler 变成 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_headerNone。如果没有任何方法被注册,最终 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,用逗号连接。注意这里没有去重——因为 ab 的方法集合不重叠(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_pathpath 参数),错误消息会包含具体路径(如 "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>>,而 InfallibleRouteFutureE 固定为 Infallible。这意味着 MethodRouter 作为 Handler 永远不会返回错误——它的错误处理已经在内部完成了(405 是正常响应,不是错误)。Infallible 的使用是 Axum 的标志性设计——当你看到一个 Infallible 在错误位置出现,就意味着"这个操作不可能失败"。

MethodRouter 作为独立服务器

MethodRouter 的 HandlerService 实现带来了一个意想不到的用途:你可以跳过 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 提供了两种添加中间件的方式:layerroute_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 返回 Selflayer 返回 MethodRouter<S, NewError>

layer 对 Allow 头的影响

无论是 layer 还是 route_layerallow_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 章将揭开嵌套与合并的机制。

基于 VitePress 构建