Axum 设计与实现

第3章 MethodRouter:HTTP 动词分发与 Allow 头

作者 杨艺韬 · 13,030 字 · 发布于 · 更新于

一个 PATCH 请求的旅程

你写了这样一行路由:

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.3.2 节明确指出,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——它所在的 method_routing.rs 有 1723 行,是整个 axum 仓库里最长的文件,也是理解请求分发机制的核心。

MethodRouter 的结构:九个字段,九个方法

打开 axum/src/routing/method_routing.rs,在 547-559 行你会看到 MethodRouter 的定义:

// axum/src/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 代表"还需要提供的状态";调用之后得到的是 MethodRouter<S2, E>,S2 与原来的 S 无关,由使用处推断(直接交给 serve 或当作 Service 用时就是 ())。E 是错误类型——当 MethodRouter 包含 service 而非 handler 时,service 的错误类型会传播到这里。纯 handler 的 MethodRouter 的 E 始终是 Infallible,因为 handler 不可能失败。

为什么不做成 HashMap<Method, Handler> 或者 Vec<(MethodFilter, Handler)>?两个原因。

第一,编译期完备性。用九个命名字段,call_with_state 先用 let Self { get, head, … } = self; 把结构体整体解构(源码注释:written with a pattern match like this to ensure we call all routes)——漏写字段是编译错误,解构出来却忘了调 call! 会得到未使用变量的警告。如果用 HashMap,你永远无法在编译期保证"所有已注册的方法都被检查了"。这个选择和 Rust 生态里"用 enum 代替字符串键"的哲学一脉相承——字段名字是编译期就确定的知识,不应该退化成运行时的字符串匹配。

第二,没有哈希开销。HashMap 的查找有哈希计算和冲突处理的开销,而命名字段的访问是直接的内存偏移量。对于"每个请求都要执行一次"的热路径来说,这个区别是有意义的——虽然以现代 CPU 的性能来看,HashMap 查找的开销微乎其微,但 Axum 选择了更确定性的方案。

结构体还有两个额外的字段:fallback 和 allow_header。fallback 处理"没有任何方法匹配"的情况,allow_header 则负责生成 405 响应中的 Allow 头。它们的故事我们稍后展开。

下面的图展示了 MethodRouter 的整体结构:

classDiagram
    class MethodRouter~S, E~ {
        +get: MethodEndpoint~S, E~
        +head: MethodEndpoint~S, E~
        +post: MethodEndpoint~S, E~
        +put: MethodEndpoint~S, E~
        +delete: MethodEndpoint~S, E~
        +patch: MethodEndpoint~S, E~
        +options: MethodEndpoint~S, E~
        +trace: MethodEndpoint~S, E~
        +connect: MethodEndpoint~S, E~
        +fallback: Fallback~S, E~
        +allow_header: AllowHeader
        +new() MethodRouter
        +on(filter, handler) Self
        +merge(other) Self
        +with_state(state) MethodRouter~S2, E~
        +layer(layer) MethodRouter~S, NewE~
        +route_layer(layer) Self
        +fallback(handler) Self
        +method_filter() Option~MethodFilter~
        +call_with_state(req, state) RouteFuture
    }
    class MethodEndpoint~S, E~ {
        <<enum>>
        None
        Route(Route~E~)
        BoxedHandler(BoxedIntoRoute~S, E~)
    }
    class Fallback~S, E~ {
        <<enum>>
        Default(Route~E~)
        Service(Route~E~)
        BoxedHandler(BoxedIntoRoute~S, E~)
    }
    class AllowHeader {
        <<enum>>
        None
        Skip
        Bytes(BytesMut)
    }
    MethodRouter --> MethodEndpoint : 9 个方法字段
    MethodRouter --> Fallback : 兜底处理
    MethodRouter --> AllowHeader : 405 响应头

MethodEndpoint:三种状态

每个方法字段都是一个 MethodEndpoint<S, E>,定义在 axum/src/routing/method_routing.rs:1272-1276:

// axum/src/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> 是 BoxCloneSyncService<Request, Response, E> 的 newtype(axum/src/routing/route.rs:31)——一个装箱的、可 clone 的 Service,已经在构造期完成了从 T: Service 到可调用服务的转换。Route 的 clone 会转调内部 service 的 Clone,对 handler 生成的 service 来说通常只是几次 Arc 引用计数增加。

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 方法(axum/src/routing/method_routing.rs:820-834),这个类型转换的逻辑一目了然:

// axum/src/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 行:

// axum/src/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——S2 由使用处推断,交给 serve 时就是 ()。

注意 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()

axum/src/routing/method_routing.rs 用两个宏批量生成了这些函数。top_level_handler_fn! 宏(105-174 行)生成 handler 版本,top_level_service_fn! 宏(27-103 行)生成 service 版本。这种宏驱动的代码生成是 Axum 源码里反复出现的模式——九个 HTTP 方法的处理逻辑几乎相同,只有方法名和 MethodFilter 常量不同,用宏消除重复是自然的选择。

最简单的入口是 get(handler)。宏模板在 165-172 行,get 本身由 441 行的 top_level_handler_fn!(get, GET); 生成,展开后等价于:

// 宏展开后的等价代码
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 就会顺带处理 HEAD(见下文);真要给 HEAD 单独的逻辑,更常见的写法是 get(handler).head(handler2)。on 的真正用途是用一个 MethodFilter 组合一次注册多个标准方法——比如 on(MethodFilter::PUT.or(MethodFilter::PATCH), upsert)。注意 MethodFilter 只能表示九个标准方法,WebDAV 这类扩展方法得走 any() 或 fallback()。

最特殊的是 any(handler)(508-515 行):

// axum/src/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):

// 宏展开后的等价代码
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 行),原因写在 871 行的注释里:// written as a separate function to generate less IR——为了让编译器少生成一些 IR(中间表示)。如果同一个方法被注册两次,它会 panic:

// axum/src/routing/method_routing.rs:885-892
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 变得难以使用。

测试用例(1608-1613 行)直接验证了 overlap panic 的行为:

// axum/src/routing/method_routing.rs:1608-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 行):

// axum/src/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 第 9.1 节要求通用服务器必须同时支持 GET 和 HEAD,而 HEAD 与 GET 的语义相同、只是不返回 body。Axum 遵守了这个规范——注册 GET handler 意味着 HEAD 也隐式可用,所以 Allow 头里必须出现 "HEAD"。

对比之下,注册 HEAD handler 时只添加 "HEAD"(909-917 行):

// axum/src/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 请求不需要 body,你可以给 HEAD 注册一个轻量 handler,而让 GET 走完整业务逻辑——不过 HEAD 响应仍应给出与 GET 相同的头(包括 Content-Length),这些值有时也得查一遍数据才知道。

MethodFilter:位运算驱动的过滤器

MethodFilter 在 axum/src/routing/method_filter.rs 中定义,只有 165 行,但它是 MethodRouter 运转的基础设施。

// axum/src/routing/method_filter.rs:8-9
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct MethodFilter(u16);

一个 u16,9 个位,每个位代表一个 HTTP 方法:

// axum/src/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 组合:

// axum/src/routing/method_filter.rs:62-64
pub const fn or(self, other: Self) -> Self {
    Self(self.0 | other.0)
}

以及包含检查:

// axum/src/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 当前注册了哪些方法(这个公开方法是主线上新加的,#3586,CHANGELOG 还挂在 Unreleased 段,不在 crates.io 的 0.8.9 里):

// axum/src/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(tokio/src/io/interest.rs:29,一个 usize newtype)用了同样的模式来表示读写兴趣的组合。这是一种在"表达组合语义"和"保持零成本"之间的经典平衡。

请求分发:call_with_state 全景

call_with_state 是 MethodRouter 最核心的方法,定义在 1167-1222 行。当你写 Router::new().route("/users", get(list).post(create)) 并且一个请求到达 /users 时,Router 先完成路径匹配,然后把请求交给对应路径的 MethodRouter::call_with_state。

// axum/src/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) 需要独占借用,而且约定每次 call 之前先 poll_ready;可 call_with_state 手里只有 &self——同一份路由表被所有请求共享,谁也拿不到它的 &mut。解决方案是:每次调用前 clone 一份 Service,交给 Oneshot 按 poll_ready → call 的顺序驱动一次,让每个请求拥有独立的 Service 实例。

《Hyper 与 Tower:工业级 HTTP 栈》专栏第 13 章讨论过相关的主题——hyper 1.x 的 Service 为什么改用 &self。那一章也澄清了一个常见误解:&mut self 并不会让 HTTP/2 退化成串行,独占只持续到 call 返回,返回的 future 并不借用 self。axum 在这里要 clone,是因为它只持有共享引用,却要满足 tower 的 &mut self 加 poll_ready 约定。Axum 全线采用 clone-based dispatch,所有 Route 和 MethodRouter 都实现了 Clone,clone 的成本通常是 Arc 引用计数的增加——对于大多数场景,这比加锁或者队列化要高效得多。

oneshot_inner_owned 的名字也值得解读。"oneshot" 表示这个 service 只会被调用一次——调用完毕就丢弃。"inner" 说明它直接对 Route 内部那个装箱的 BoxCloneSyncService 做 oneshot,不经过 Route 自己的 Service::call(那条路径会把 future 标成非顶层,axum/src/routing/route.rs:102-105);Oneshot 本身仍会先 poll_ready 再 call(tower/src/util/oneshot.rs:91-92)。"owned" 表示它消费了 self(通过 clone 获得),而不是用 &mut self 调用。这三个词合在一起精确描述了"clone 一次、调用一次、丢弃"的 dispatch 模式。

BoxedHandler 分支多了一步:handler.clone().into_route(state)。这是"注入 state 并转为 Route"的延迟操作——只有到真正需要调用时才执行。注意这个分支每次调用都要执行一次 into_route。它只出现在还没调用过 with_state 的 MethodRouter 上——状态类型是 () 也不例外:get(handler) 得到的 MethodRouter<()> 里存的仍是 BoxedHandler,直接当 tower Service 调用时,每个请求都会走这条分支。走 axum::serve 时,每个新连接都会先 with_state(()) 一次(axum/src/routing/method_routing.rs:1384,Router 那边见 axum/src/routing/mod.rs:592-594,注释写明就是为了把一切提前转成 Route、而不是每个请求再转),此后该连接上的请求走的都是 Route 分支。

分发顺序:HEAD 先于 GET

注意分发顺序(1204-1213 行):

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-1444 行)直接验证了这个行为:

// axum/src/routing/method_routing.rs:1439-1444 (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 行):

// axum/src/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 里做的——它发生在 RouteFuture::poll 里(axum/src/routing/route.rs:163-175):当这个 future 处于顶层(top_level,即外面没有再包一层 Route)且请求方法是 HEAD 时,axum 先按响应体的 size_hint 补好 Content-Length,再把 body 换成 Body::empty()。顺序很关键——注释里写着 // make sure to set content-length before removing the body:HEAD 响应若带 Content-Length,其值必须等于同一请求用 GET 时的 body 长度(RFC 9110 第 8.6 节),先按原 body 补上,客户端才知道这个资源有多大。

分发顺序中还有一个微妙之处:HEAD 请求的 call! 宏调用出现了两次——一次匹配 head 字段,一次匹配 get 字段。这是因为宏的第一个参数 $method_variant 是固定的 HEAD,第二个参数 $svc 分别是 head 和 get。同一个请求方法可以检查多个字段,这是 call! 宏的设计意图——它不是"一个方法只查一个字段"的简单映射,而是一个灵活的"方法→字段"查找序列。除了 HEAD 的特殊处理外,其他方法严格遵循一对一映射:POST 只查 post 字段,DELETE 只查 delete 字段,以此类推。这种"大部分一对一、HEAD 特殊"的模式恰好反映了 HTTP 协议的实际情况——HEAD 是唯一一个"可以由另一个方法的 handler 隐式处理"的方法。

全部不匹配:走 fallback

如果请求方法不匹配任何已注册的 endpoint,控制流到达 1215 行:

let future = fallback.clone().call_with_state(req, state);

默认的 fallback 在 MethodRouter::new() 中设置(799-817 行):

// axum/src/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|),只返回状态码——fallback 本身产生的 405 响应没有 body,也不带任何 header;Allow 头是之后由 RouteFuture 补上的(见下文)。

你可以用 .fallback(handler) 替换默认 fallback。设置自定义 fallback 后,所有不匹配的方法都会走这个 handler。这也是 any(handler) 的实现方式——它把 handler 设为 fallback,所有方法都不匹配 → 走 fallback → 等于所有方法都走这个 handler。

Fallback:三层兜底

Fallback<S, E> 定义在 axum/src/routing/mod.rs:710-714:

// axum/src/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 行)清晰地展示了三种变体的调用路径:

// axum/src/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-749 行)的实现也遵循同样的模式: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 行:

// axum/src/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 行:

// axum/src/routing/method_routing.rs:1126-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() 方法(axum/src/routing/mod.rs:761-763)用来判断 fallback 是否是默认的——method_filter() 方法在 fallback 不是 Default 时返回 None。

AllowHeader:405 响应的灵魂

405 响应如果不带 Allow 头,客户端就不知道这个路径支持哪些方法。RFC 9110 第 15.5.6 节要求 405 响应必须包含 Allow 头。Axum 的 AllowHeader 就是为此而生。

// axum/src/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,好处在出口:freeze() 成 Bytes 之后,经 HeaderValue::from_maybe_shared 就能直接做成头部值、不再复制(axum/src/routing/route.rs:187)。它并没有省掉 UTF-8 检查——下面的 append_allow_header 每次追加前都会 std::str::from_utf8 一遍;追加用的是 extend_from_slice,把逗号和方法名拷到已有字节后面。

构建过程:每次注册方法时追加

append_allow_header 函数(1225-1243 行)负责在注册新方法时追加 Allow 值:

// axum/src/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());
                }
            } else {
                #[cfg(debug_assertions)]
                panic!("`allow_header` contained invalid utf-8. This should never happen")
            }
        }
    }
}

以 get(handler) 为例:set_endpoint 中 GET 的 methods 参数是 &["GET", "HEAD"],于是连续追加两次——第一次把 None 变成 BytesMut::from("GET"),第二次在后面追加 ",HEAD"。

去重逻辑在 !s.contains(method) ——防止同一个方法名被添加两次。注意这里用的是子串 contains 而不是按逗号切分后精确匹配。这样做之所以不出错,是因为这里只会追加九个标准方法名,而它们两两之间都不是对方的子串;换成任意扩展方法就不成立了——"GET" 恰好是 "TARGET" 的子串。

Skip 状态下 append_allow_header 什么都不做——一旦进入 Skip 状态,任何追加操作都被忽略。这是因为 Skip 代表"不需要 Allow 头",这个决定是终局的——即使后来链式调用了 .get(handler),AllowHeader 仍然保持 Skip。这符合 any() 的语义:当你用 any() 声明"接受所有方法"后,即使又注册了具体方法,Allow 头的存在也不会增加信息量。

合并时的 AllowHeader

当两个 MethodRouter 合并时,AllowHeader 也要合并(1131 行):

self.allow_header = self.allow_header.merge(other.allow_header);

merge 方法的实现在 572-584 行:

// axum/src/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 行)验证了合并行为:

// axum/src/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,用逗号连接。注意这里没有去重。merge 的前置条件只保证两边的方法字段不重叠,并不保证 Allow 值不重叠——注册 GET 时会顺带写入 "HEAD"。所以 get(ok).merge(head(ok)) 能合并成功,405 响应里却是 Allow: GET,HEAD,HEAD(axum 0.8.9 实测)。重复的方法名不影响客户端理解,但也说明这里的拼接并不保证唯一。

在 405 响应中写入 Allow 头

回到 call_with_state 的结尾(1217-1221 行):

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 值,响应就绪后再检查:只有在顶层、响应状态确实是 405、且响应里还没有 Allow 头时,才把它插进去(axum/src/routing/route.rs:164-168、182-192)。

allow_header.clone().freeze() 把 BytesMut 转换成 Bytes——前者是可变的,后者是不可变的。freeze 操作不需要拷贝数据,只是改变了元数据的标记。

测试用例(1514-1519 行)完整验证了 Allow 头的行为:

// axum/src/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.3.2 节的原话是:

The HEAD method is identical to GET except that the server MUST NOT send content 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),随后 RouteFuture::poll 会在顶层把 HEAD 响应的 body 换成空(axum/src/routing/route.rs:173-175)。

不冲突的设计:get(ok).head(ok) 不会 panic,因为 GET 和 HEAD 的字段是独立的。set_endpoint 只在同一个字段上重复注册时才 panic。这允许你给 HEAD 注册一个轻量级的 handler(不生成 body,但头要与 GET 一致),同时让 GET 走完整的业务逻辑。

Allow 头的正确性:如果你只注册了 GET handler,一个 DELETE 请求返回的 405 响应中 Allow 头应该包含 "GET,HEAD"。Axum 在注册 GET 时就把 "HEAD" 加入了 AllowHeader,所以这是自动满足的。测试用例(1522-1527 行)验证了这个行为:

// axum/src/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 交互的完整状态机:

stateDiagram-v2
    state "仅 GET" as OnlyGet
    state "GET+HEAD 复用" as GetHead
    state "GET+专用 HEAD" as GetDedicatedHead
    state "仅 HEAD" as OnlyHead
    [*] --> OnlyGet : get(handler)
    OnlyGet --> GetHead : (自动派生)
    [*] --> OnlyHead : head(handler)
    GetHead --> GetDedicatedHead : .head(head_handler)
    OnlyHead --> GetDedicatedHead : .get(get_handler)
    note right of GetHead
        Allow: GET,HEAD
        HEAD 请求走 GET handler
        body 由 RouteFuture 剥除
    end note
    note right of OnlyHead
        Allow: HEAD
        HEAD 走专用 handler
        GET 请求得到 405
    end note
    note right of GetDedicatedHead
        Allow: GET,HEAD
        HEAD 走 head_handler
        GET 走 get_handler
    end note

merge:两个 MethodRouter 的组合

merge 方法(1138-1144 行)是 MethodRouter 的另一个核心操作。它的典型用例是:不同的模块各自定义一个 MethodRouter,最后在 Router 层面合并。

// axum/src/routing/method_routing.rs:1138-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-1114 行)。规则是:如果一方是 None,取非 None 的一方;如果双方都有值,报 overlap 错误。

// axum/src/routing/method_routing.rs:1090-1114
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 的 9 次 merge_inner 调用(1116-1124 行)逐一处理每个方法字段。虽然代码看起来是重复的,但这种展开式的写法保证了每个方法都被处理——和 call_with_state 中的九次 call! 宏调用一样,用代码的冗长换取完备性的保证。

测试用例(1447-1458 行)验证了正常合并的行为:

// axum/src/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(axum/src/routing/method_routing.rs:1355-1364,impl 上方还挂着一个 #[diagnostic::do_not_recommend],第 1354 行):

// axum/src/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 能嵌套的根本原因。当你写:

Router::new().route("/api", any(api_routes))

any(api_routes) 把 api_routes(一个 MethodRouter)设为 fallback——any 的函数体就一行 MethodRouter::new().fallback(handler).skip_allow_header()(axum/src/routing/method_routing.rs:508-515),而 api_routes 能成为 fallback 的参数,正是因为它实现了 Handler。这里 Handler trait 的 marker 类型参数 T 是 (),意味着 MethodRouter 不需要任何额外的提取器——它自己就是完整的分发器。

MethodRouter 同时也实现了 Service<Request<B>>(impl<B, E> Service<Request<B>> for MethodRouter<(), E>,axum/src/routing/method_routing.rs:1333-1352),但只限 S = () 的状态——即不需要 state 的 MethodRouter。这是合理的:tower::Service 没有"注入 state"的概念,所以只有不需要 state 的 MethodRouter 才能直接作为 Service 使用。需要 state 的 MethodRouter 必须先调用 with_state,然后才能作为 Service。

InfallibleRouteFuture 的使用也值得注意。Handler::Future 要求 Future<Output = Response>,而 call_with_state 返回的 RouteFuture<E> 输出的是 Result<Response, E>;InfallibleRouteFuture(axum/src/routing/route.rs:214-237)包住一个 RouteFuture<Infallible>,把永远不会出现的 Err 分支抹掉,直接输出 Response。这意味着 MethodRouter 作为 Handler 永远不会返回错误——它的错误处理已经在内部完成了(405 是正常响应,不是错误)。Infallible 的使用是 Axum 的标志性设计——当你看到一个 Infallible 在错误位置出现,就意味着"这个操作不可能失败"。

MethodRouter 作为独立服务器

MethodRouter 的 Handler 和 Service 实现带来了一个意想不到的用途:你可以跳过 Router,直接用 MethodRouter 启动一个 HTTP 服务。into_make_service 方法(axum/src/routing/method_routing.rs:754-756)为此提供了支持:

// axum/src/routing/method_routing.rs:754-756
pub fn into_make_service(self) -> IntoMakeService<Self> {
    IntoMakeService::new(self.with_state(()))
}

这使得以下代码成为可能:

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 方法(axum/src/routing/method_routing.rs:788-790,带 #[cfg(feature = "tokio")])在此基础上还支持获取客户端连接信息(如 IP 地址)。

layer 与 route_layer:两种中间件策略

MethodRouter 提供了两种添加中间件的方式:layer 和 route_layer。它们的区别在于中间件是否覆盖 fallback。

layer:全方法覆盖

layer 方法(1014-1040 行)对 MethodRouter 的所有端点应用中间件——包括 fallback:

// axum/src/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 行)验证了这个行为:

// axum/src/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 方法(1044-1082 行)只对已注册的 endpoint 应用中间件,不影响 fallback。它还会检查是否已注册了任何 endpoint(1053-1067 行),如果没有则 panic——对空路由添加中间件是一个无操作,很可能是 bug:

// axum/src/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."
    );
}

测试用例(1478-1490 行)验证了这个行为:

// axum/src/routing/method_routing.rs:1478-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 未被包裹)。

两种策略怎么选?upstream 的默认建议是鉴权走 route_layer:route_layer 的文档(由 axum/src/routing/method_routing.rs:1042 的 #[doc = include_str!(...)] 引入)写得很直白——它"适合会提前返回的中间件(比如鉴权),否则那类中间件会把 405 Method Not Allowed 变成 401 Unauthorized"。第 2 章和第 13 章讲 Router 那一侧时用的也是这个口径(那边被改写的是 404 Not Found)。

反过来用 layer 包住 fallback 只在一种情况下才划算:你刻意不想让未认证的客户端从 405 里读出"这个路径存在、只是方法不对"。代价是所有 405 都变成 401,客户端拿不到 Allow 头里的可用方法,调试和正常的 API 探测一起被牺牲掉。这是一个要显式权衡的选择,不是默认做法。限流中间件则通常用 route_layer——只对实际处理请求的方法限流,避免对 405 响应浪费限流配额。

route_layer 不改变错误类型 E——因为中间件的错误类型必须和原有的一致。这也是为什么 route_layer 返回 Self 而 layer 返回 MethodRouter<S, NewError>。

layer 对 Allow 头的影响

无论是 layer 还是 route_layer,allow_header 字段都不会被修改——它原样传递。这意味着中间件不会影响 Allow 头的内容。测试用例(1597-1605 行)验证了这一点:

// axum/src/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 到最终返回响应的完整流程如下:

flowchart TD
    A[请求到达 MethodRouter] --> B{请求方法是 HEAD?}
    B -->|是| C{head 字段有 handler?}
    C -->|有| D[调用 head handler]
    C -->|没有| E{get 字段有 handler?}
    E -->|有| F[调用 get handler\nRouteFuture 剥除 body]
    E -->|没有| G[继续匹配其他方法]
    B -->|否| H{请求方法匹配\n对应字段?}
    H -->|匹配| I{该字段有 handler?}
    I -->|Route| J[clone Route 并 oneshot]
    I -->|BoxedHandler| K[注入 state 转为 Route\n再 oneshot]
    I -->|None| G
    H -->|不匹配| G
    G --> L{所有方法都不匹配?}
    L -->|是| M[调用 fallback]
    M --> N{AllowHeader 状态?}
    N -->|None| O[405 + 空 Allow 头\nfallback 返回 405 时]
    N -->|Skip| P[正常返回\n不设 Allow 头]
    N -->|Bytes| Q[405 + 具体的 Allow 值\nfallback 返回 405 时]
    L -->|否| R[不应到达此处]
    style D fill:#90EE90
    style F fill:#90EE90
    style J fill:#90EE90
    style K fill:#90EE90
    style O fill:#FFB6C1
    style Q fill:#FFB6C1

这个流程图揭示了一个重要的事实:MethodRouter 的分发是线性扫描而非哈希查找。最多需要检查 10 次(HEAD 先查 head 再查 get,加上其余 8 个方法)的方法比较才能确定走哪个 endpoint。每个 call! 都会先比较请求方法,所以比较次数只取决于请求方法在这串检查里的位置(GET 是第 3 次,CONNECT 是第 10 次),与这个路径注册了几个方法无关。在方法总数只有九个的情况下,线性扫描不需要哈希计算、没有冲突处理,缓存局部性也更好。

Endpoint:Router 视角的 MethodRouter

在 axum/src/routing/mod.rs:787-790,有一个 Endpoint<S> 枚举:

// axum/src/routing/mod.rs:787-790
enum Endpoint<S> {
    MethodRouter(MethodRouter<S>),
    Route(Route),
}

这是 Router 内部存储路径端点的数据结构。当你写 .route("/users", get(list).post(create)) 时,Router 在 /users 路径下存储一个 Endpoint::MethodRouter(...)。只有通过 .route_service(path, svc)、.nest_service() 这类直接挂 Service 的入口,Router 才会存储 Endpoint::Route(...)(axum/src/routing/path_router.rs:117、235)——.route() 永远存 Endpoint::MethodRouter。

Endpoint::layer 方法(796-808 行)展示了两种变体的中间件应用差异:

// axum/src/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 使用。

// axum/src/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<S2>(交给 serve 时推断为 ()),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 章将揭开嵌套与合并的机制。