nest、merge 与 fallback
主线位置:主要在装配主线。
nest和merge都不保留子路由器这个「整体」,而是把子路由器里的路由一条条搬进父路由器(nest_service例外),fallback 也一样被搬进父路由器的第二棵树。请求主线上能看到的只是结果:一次 matchit 匹配、一层StripPrefix改写 URI,以及没匹配上时按顺序查的三级兜底。
flowchart TB req["请求"] --> pr["path_router:普通路由"] pr -->|命中| ep["端点:MethodRouter 或 Route"] ep -->|"方法不对"| m405["MethodRouter 的 fallback:405"] pr -->|NotFound| fr["fallback_router"] fr -->|"命中子前缀"| inner["子路由器的 fallback"] fr -->|"命中顶层通配"| outer["本路由器的 fallback"] fr -->|NotFound| ca["catch_all_fallback"]
RouterInner 有四个字段:path_router、fallback_router、default_fallback、catch_all_fallback。它们各自是什么、Router::call_with_state 怎么依次尝试,见 Router 的内部结构。这一页讲 nest、merge、fallback 怎么改这几个字段。
nest:拆开、加前缀、逐条注册
pub fn nest(self, path: &str, router: Router<S>) -> Self {
if path.is_empty() || path == "/" {
panic!("Nesting at the root is no longer supported. Use merge instead.");
}
let RouterInner {
path_router,
fallback_router,
default_fallback,
// we don't need to inherit the catch-all fallback. It is only used for CONNECT
// requests with an empty path. If we were to inherit the catch-all fallback
// it would end up matching `/{path}/*` which doesn't match empty paths.
catch_all_fallback: _,
} = router.into_inner();
tap_inner!(self, mut this => {
panic_on_err!(this.path_router.nest(path, path_router));
if !default_fallback {
panic_on_err!(this.fallback_router.nest(path, fallback_router));
}
})
}
子路由器被拆成零件:普通路由进父路由器的 path_router;子路由器有自定义 fallback 时(!default_fallback),它的 fallback_router 也进父路由器的 fallback_router。两步用的是同一个 PathRouter::nest:对子路由的每条路径调用 path_for_nested_route 拼上前缀,给端点套上 (StripPrefix::layer(prefix), SetNestedPath::layer(path_to_nest_at)),再像普通的 route 一样注册。之后父路由器里没有「子路由器」,只有一批带前缀的路由。
拼前缀有一条特例:子路由器的 / 拼出来是前缀本身,不是「前缀 + /」:
if prefix.ends_with('/') {
format!("{prefix}{}", path.trim_start_matches('/')).into()
} else if path == "/" {
prefix.into()
} else {
format!("{prefix}{path}").into()
}
nest_service 则不拆。服务是不透明的,axum 不知道里面有哪些路径,只能在父路由树里注册 {prefix}/{*__private__axum_nest_tail_param},再补上 {prefix} 和 {prefix}/ 两条(源码注释:「/{*rest} is not matched by / so we need to also register a router at the prefix itself」)。三条都指向同一个套了 StripPrefix 的 Route;如果服务本身是一个 Router,它收到剥过前缀的请求后自己再匹配一次。
同一个只有 / 路由的子路由器,分别挂到 /n 和 /s:
axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["full"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "0.1"
use axum::{Router, body::Body, http::{Request, Uri}, routing::get};
use http_body_util::BodyExt;
use tower::ServiceExt;
#[tokio::main]
async fn main() {
let inner = || Router::new().route("/", get(|uri: Uri| async move { format!("inner / sees {uri}") }));
let app = Router::new().nest("/n", inner()).nest_service("/s", inner());
for uri in ["/n", "/n/", "/s", "/s/", "/s/x"] {
let req = Request::get(uri).body(Body::empty()).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let status = res.status().as_u16();
let body = res.into_body().collect().await.unwrap().to_bytes();
println!("{uri:<5} {status} {:?}", String::from_utf8_lossy(&body));
}
}
M2 Max、rustc 1.99.0、--release,连跑 3 次,输出相同:
/n 200 "inner / sees /"
/n/ 404 ""
/s 200 "inner / sees /"
/s/ 200 "inner / sees /"
/s/x 404 ""
nest下子路由的/变成了/n,所以/n/是 404,和 matchit 与路径语法 里讲的尾斜杠规则一致。nest.md文档里也写了:「a nested router at/foowill match the path/foo(but not/foo/)」。nest_service下/s和/s/都进了子路由器,剥前缀后都是/。/s/x也进了子路由器,是子路由器自己回的 404。
StripPrefix:子路由看到的路径
StripPrefix 在每次调用时改写 req.uri_mut()。strip_prefix 把请求路径和前缀模板按段对齐:前缀段是参数({version})就匹配任意段,否则要字面相等;数出前缀占了多少字节,切掉,剩下的部分不以 / 开头就补一个,查询串原样接回去。前缀里可以有参数,正是因为这里按模板而不是按字面比较。
结果是:nest 和 nest_service 下,handler 的 Uri 提取器看到的都是剥过前缀的路径,OriginalUri 才是原始的;MatchedPath 两种挂法都是完整模板。这些在 Path、Query 与请求元数据 里已经用同一个子路由器、两种挂法对比过,这里不重复。两种挂法真正的区别在三处:
nest(path, Router) | nest_service(path, svc) | |
|---|---|---|
| 父路由树里注册什么 | 子路由的每一条,各加前缀 | {path}、{path}/、{path}/{*tail} 三条 |
| 每个请求匹配几次 | 一次 | 两次(父树一次,子 Router 自己一次) |
/{path} 和 /{path}/ | 取决于子路由器有没有 / | 都进服务 |
| 子路由器的 fallback | 搬进父路由器,没有就继承父路由器的 | 留在子路由器里,不继承 |
能用 nest 时用 nest。nest_service 是给不透明的服务准备的,典型的是 tower-http 的 ServeDir(见 ServeDir 与 ServeFile)。把 Router 交给 route_service 会直接 panic,提示改用 nest(见下面的输出)。
什么情况会 panic
nest、nest_service、merge 最终都是往 matchit 里插路由,冲突的规则和 route 一样;它们自己还有几条检查。挨个试:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
use std::panic::{self, AssertUnwindSafe};
use axum::{Router, routing::get};
fn try_build(label: &str, f: impl FnOnce() -> Router) {
match panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(_) => println!("{label}\n ok"),
Err(e) => {
let msg = e
.downcast_ref::<String>()
.cloned()
.or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default();
println!("{label}\n panic: {msg}");
}
}
}
fn main() {
panic::set_hook(Box::new(|_| {}));
let h = || get(|| async {});
let inner = || Router::new().route("/users", get(|| async {}));
let svc = || tower::service_fn(|_| async { Ok::<_, std::convert::Infallible>("svc") });
try_build("nest(\"/\", inner)", || Router::new().nest("/", inner()));
try_build("nest_service(\"/\", svc)", || Router::new().nest_service("/", svc()));
try_build("nest(\"/files/{*rest}\", inner)", || Router::new().nest("/files/{*rest}", inner()));
try_build("nest(\"/:version\", inner)", || Router::new().nest("/:version", inner()));
try_build("nest(\"/api\", a).nest(\"/api\", b)", || {
Router::new().nest("/api", inner()).nest("/api", Router::new().route("/teams", h()))
});
try_build("nest(\"/api\", inner).route(\"/api/users\", ..)", || {
Router::new().nest("/api", inner()).route("/api/users", h())
});
try_build("nest_service(\"/api\", svc).route(\"/api\", ..)", || {
Router::new().nest_service("/api", svc()).route("/api", h())
});
try_build("route(\"/users\", get).merge(route(\"/users\", post))", || {
Router::new()
.route("/users", h())
.merge(Router::new().route("/users", axum::routing::post(|| async {})))
});
try_build("route(\"/users\", get).merge(route(\"/users\", get))", || {
Router::new().route("/users", h()).merge(Router::new().route("/users", h()))
});
try_build("route(\"/{id}\").merge(route(\"/{name}\"))", || {
Router::new().route("/{id}", h()).merge(Router::new().route("/{name}", h()))
});
try_build("a.fallback(f).merge(b.fallback(f))", || {
Router::new()
.fallback(|| async {})
.merge(Router::new().fallback(|| async {}))
});
try_build("a.fallback(f).merge(b.fallback(f).reset_fallback())", || {
Router::new()
.fallback(|| async {})
.merge(Router::new().fallback(|| async {}).reset_fallback())
});
try_build("route_service(\"/api\", Router)", || {
Router::new().route_service("/api", inner())
});
try_build("nest(\"/api\", a.fallback(f)).nest(\"/api\", b.fallback(f))", || {
Router::new()
.nest("/api", inner().fallback(|| async {}))
.nest("/api", Router::new().route("/teams", h()).fallback(|| async {}))
});
}
连跑 3 次,输出相同:
nest("/", inner)
panic: Nesting at the root is no longer supported. Use merge instead.
nest_service("/", svc)
panic: Nesting at the root is no longer supported. Use fallback_service instead.
nest("/files/{*rest}", inner)
panic: Invalid route: nested routes cannot contain wildcards (*)
nest("/:version", inner)
panic: called `Result::unwrap()` on an `Err` value: "Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router."
nest("/api", a).nest("/api", b)
ok
nest("/api", inner).route("/api/users", ..)
panic: Overlapping method route. Handler for `GET /api/users` already exists
nest_service("/api", svc).route("/api", ..)
panic: Invalid route "/api": Insertion failed due to conflict with previously registered route: /api
route("/users", get).merge(route("/users", post))
ok
route("/users", get).merge(route("/users", get))
panic: Overlapping method route. Handler for `GET /users` already exists
route("/{id}").merge(route("/{name}"))
panic: Invalid route "/{name}": Insertion failed due to conflict with previously registered route: /{id}
a.fallback(f).merge(b.fallback(f))
panic: Cannot merge two `Router`s that both have a fallback
a.fallback(f).merge(b.fallback(f).reset_fallback())
ok
route_service("/api", Router)
panic: Invalid route: `Router::route_service` cannot be used with `Router`s. Use `Router::nest` instead
nest("/api", a.fallback(f)).nest("/api", b.fallback(f))
panic: Cannot merge two `MethodRouter`s that both have a fallback
- 在根上 nest:
Router::nest和Router::nest_service一开头就拦下""和"/",并分别指向merge和fallback_service。 - 前缀里不能有通配:
validate_nest_path检查。它对旧语法的检查用的是validate_v07_paths(path).unwrap(),所以同一句提示在这里被包成了called `Result::unwrap()` on an `Err` value: "...",不像route那样干净。 - 同一前缀 nest 两次是可以的,只要两边的路由不撞:拍平之后它们只是几条不同的路径。0.6.0 的 CHANGELOG 写过「
.nest("/foo", a).nest("/foo", b)will panic」,在 0.8.9 里已经不是这样。 - 和 nest 进来的路由撞车,报错与直接
route两次完全相同:父路由器里本来就只是两条同路径的路由。 merge的冲突:PathRouter::merge对另一边的每条路由调用self.route或self.route_service,同路径不同方法会合并,同方法、同位置参数都 panic,和 MethodRouter、matchit 与路径语法 里的报错一致。
nest("/", ...) 曾经是合法的,0.8 才禁止#3051(2024-11-26 合并,首次发布于 axum-v0.8.0-rc.1,正式版 axum-v0.8.0)加上了上面那两句 panic。PR 给的理由只有一句:「It's confusing, there isn't really any nesting going on.」在根上 nest 既没有前缀可剥,也没有前缀可加,效果和 merge 一样;nest_service 在根上则等于「所有没匹配的请求都给这个服务」,就是 fallback_service。PR 的原话:「merge can fully replace nest("" | "/", ...), fallback_service can fully replace nest_service("" | "/", ...).」于是 panic 信息直接告诉你该换成哪一个。
merge:两棵树合成一棵
Router::merge 先把 path_router 合并(上面说的逐条 route),再按两边是否有自定义 fallback 分四种情况合并 fallback_router:
match (this.default_fallback, default_fallback) {
// both have the default fallback
// use the one from other
(true, true) => {
this.fallback_router.merge(other_fallback).expect(PANIC_MSG);
}
// this has default fallback, other has a custom fallback
(true, false) => {
this.fallback_router.merge(other_fallback).expect(PANIC_MSG);
this.default_fallback = false;
}
// this has a custom fallback, other has a default
(false, true) => {
let fallback_router = std::mem::take(&mut this.fallback_router);
other_fallback.merge(fallback_router).expect(PANIC_MSG);
this.fallback_router = other_fallback;
}
// both have a custom fallback, not allowed
(false, false) => {
panic!("Cannot merge two `Router`s that both have a fallback")
}
};
每个 fallback_router 都有 / 和 /{*__private__axum_fallback} 两条顶层路由,直接合并必然冲突。PathRouter::merge 在 IS_FALLBACK 为真时对这两条路径改用 replace_endpoint,用另一边的端点覆盖自己的;Router::merge 负责让「另一边」总是想保留的那一个。所以 a.merge(b) 和 b.merge(a) 的 fallback 结果相同(源码注释:「when merging two routers it doesn't matter if you do a.merge(b) or b.merge(a). This must also be true for fallbacks」)。两边都有自定义 fallback 时没法选,panic;要保留其中一个,先对另一个调 reset_fallback。catch_all_fallback 用 Fallback::merge 按同样的规则取舍。
fallback 在哪里、谁继承谁
Router::fallback 做两件事:把 handler 存成 catch_all_fallback,再把 any(handler) 交给 fallback_endpoint,后者调用 set_fallback:
pub(super) fn set_fallback(&mut self, endpoint: Endpoint<S>) {
self.replace_endpoint("/", endpoint.clone());
self.replace_endpoint(FALLBACK_PARAM_PATH, endpoint);
}
也就是在 fallback_router 这棵树里,把 / 和 /{*__private__axum_fallback} 两条路由换成你的 handler,并把 default_fallback 设成 false。fallback 不是一个特殊的槽,而是第二棵路由树里的两条普通路由。
这样继承就有了很直接的实现。子路由器有自定义 fallback 时,nest 把它的 fallback_router 也加前缀搬过来,父路由器的 fallback 树里多出 /b 和 /b/{*__private__axum_fallback};请求 /b/x 在普通路由树里没匹配上,到 fallback 树里,matchit 的静态优先让 /b/{*..} 赢过 /{*..},于是子路由器的 fallback 接住。子路由器没有自定义 fallback 时什么都不搬,/a/x 只能匹配父路由器的 /{*..},用的就是父路由器的 fallback。nest_service 不碰 fallback 树,子 Router 的兜底留在它自己里面。
#1711(2023-04-11 合并,首次发布于 axum-v0.6.13)把 nest 改成拍平:「just moving the routes from one router to another by calling .route(nest_path, nested_endpoint)」。CHANGELOG 记的理由是修 0.6.0 引入的性能回退(「nest now flattens the routes which performs better」)。难点在 fallback 继承,PR 描述说:「The main reason we didn't flatten routes in 0.6 previously was specifically because of fallback inheritance」。解决办法就是本页的结构:新增内部类型 PathRouter,「A Router then contains two PathRouters: One for the actual routes and another for fallbacks. Then when you add a fallback to the root router that adds / and /* routes to the fallback PathRouter. Nesting is then pretty straight forward: We just nest the routes from each PathRouter.」
继承规则本身改过几次:0.4 起 nest 一个带 fallback 的路由器会 panic(#529,此前 fallback 被静默丢弃);0.6.0-rc.1 的 #1086 让嵌套路由器不再委托给外层 fallback;0.6.0-rc.5 的 #1521 又加回继承,并允许子路由器有自己的 fallback;0.7.0 的 #1956 规定只有 nest 继承、nest_service 不继承,理由是「It also simplifies the code and avoids some unfortunate extension work arounds」。
把四种挂法放在一起:
axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["full"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "0.1"
use axum::{
Router,
body::Body,
http::{Method, Request, StatusCode, Uri},
routing::get,
};
use http_body_util::BodyExt;
use tower::ServiceExt;
fn fb(name: &'static str) -> impl Fn(Uri) -> std::future::Ready<String> + Clone {
move |uri: Uri| std::future::ready(format!("{name} sees {uri}"))
}
async fn send(app: &Router, method: Method, uri: &str) {
let req = Request::builder().method(method.clone()).uri(uri).body(Body::empty()).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let status = res.status().as_u16();
let body = res.into_body().collect().await.unwrap().to_bytes();
println!("{:<5}{uri:<10} {status} {:?}", method.as_str(), String::from_utf8_lossy(&body));
}
#[tokio::main]
async fn main() {
let users = || Router::new().route("/users", get(|| async { "users" }));
let app = Router::new()
.nest("/a", users())
.nest("/b", users().fallback(fb("b_fallback")))
.nest_service("/c", users())
.nest_service("/d", users().fallback(fb("d_fallback")))
.fallback(fb("outer_fallback"));
for uri in ["/a/x", "/b/x", "/c/x", "/d/x", "/x", "/a", "/b", "/b/", "/c"] {
send(&app, Method::GET, uri).await;
}
send(&app, Method::POST, "/a/users").await;
println!("-- fallback set before nest");
let app = Router::new().fallback(fb("outer_fallback")).nest("/a", users());
send(&app, Method::GET, "/a/x").await;
println!("-- method_not_allowed_fallback before / after nest");
let m405 = |m: Method| async move { (StatusCode::METHOD_NOT_ALLOWED, format!("custom 405 for {m}")) };
let before = Router::new().method_not_allowed_fallback(m405).nest("/a", users());
let after = Router::new().nest("/a", users()).method_not_allowed_fallback(m405);
send(&before, Method::POST, "/a/users").await;
send(&after, Method::POST, "/a/users").await;
println!("-- merge keeps whichever side has the fallback");
let left = users().fallback(fb("left_fallback")).merge(Router::new().route("/teams", get(|| async { "teams" })));
let right = Router::new().route("/teams", get(|| async { "teams" })).merge(users().fallback(fb("right_fallback")));
send(&left, Method::GET, "/x").await;
send(&right, Method::GET, "/x").await;
}
连跑 3 次输出相同,完整输出在本页最后。整理成表(/b/ 那一行留给动脑筋):
| 请求 | 挂法 | 结果 |
|---|---|---|
GET /a/x | nest,子路由器无 fallback | 200 outer_fallback sees /a/x |
GET /b/x | nest,子路由器有 fallback | 200 b_fallback sees /x |
GET /c/x | nest_service,子路由器无 fallback | 404,空 body |
GET /d/x | nest_service,子路由器有 fallback | 200 d_fallback sees /x |
GET /x | 不在任何前缀下 | 200 outer_fallback sees /x |
GET /a | 子路由器没有 / 路由 | 200 outer_fallback sees /a |
GET /b | 同上,但子路由器有 fallback | 200 b_fallback sees / |
GET /c | nest_service | 404,空 body |
POST /a/users | 路径存在、方法不对 | 405,空 body |
GET /b/ 由哪个 fallback 处理?它看到的 URI 是什么?提示:子路由器挂在 /b,自己有 fallback;父路由器也有 fallback。M2 Max、rustc 1.99.0。答案在本页最后。
nest继承,nest_service不继承。/c/x的 404 是子Router自己默认的NotFound,父路由器的 fallback 根本没机会。- 子路由器的 fallback 也看到剥过前缀的 URI(
/x、/),因为它和普通路由一样经过PathRouter::nest,套了StripPrefix;父路由器的 fallback 看到的是完整路径。 /b进了子路由器的 fallback:子 fallback 树里的/按path_for_nested_route的特例拼成了/b。- 405 不归
Router::fallback管。POST /a/users的路径匹配上了,交给MethodRouter,由它的 fallback 回 405(fallback.md文档原话:「if the request hits a valid path but theMethodRouterdoes not have an appropriate method handler installed, the fallback is not called」)。 fallback写在nest前后都一样(输出的「fallback set before nest」一段):set_fallback只替换顶层的两条,nest搬进来的/a/{*..}不受影响。method_not_allowed_fallback有顺序。PathRouter::method_not_allowed_fallback遍历的是调用那一刻routes里已有的MethodRouter,写在nest之前时,后来搬进来的路由没有被改,仍然是默认的空 405。merge的两种方向都保留了唯一的那个 fallback(left_fallback、right_fallback)。
问: 两个都带 fallback 的子路由器 nest 到同一个 /api,报错是 Cannot merge two `MethodRouter`s that both have a fallback。我没有合并任何 MethodRouter,这句话从哪来?
答: 从 fallback 的存法来。Router::fallback 往 fallback 树里放的是 Endpoint::MethodRouter(any(handler)),而 any 就是一个只有 fallback 槽的 MethodRouter。第二次 nest 时,两边都要往父路由器的 fallback 树里注册 /api 和 /api/{*__private__axum_fallback};PathRouter::route 发现同一路径已有 MethodRouter,就照常走 merge_for_path 去合并,两边的 fallback 槽都被占了,于是报这句。换句话说,同一前缀下只能有一个子路由器带 fallback。把这些路由放进同一个子路由器、只给它设一个 fallback;两个都带 fallback 的路由器直接 merge 同样会 panic,只是报的是「two Routers」那句,更好懂。
动脑筋答案
fallback 程序的完整输出,连跑 3 次相同:
GET /a/x 200 "outer_fallback sees /a/x"
GET /b/x 200 "b_fallback sees /x"
GET /c/x 404 ""
GET /d/x 200 "d_fallback sees /x"
GET /x 200 "outer_fallback sees /x"
GET /a 200 "outer_fallback sees /a"
GET /b 200 "b_fallback sees /"
GET /b/ 200 "outer_fallback sees /b/"
GET /c 404 ""
POST /a/users 405 ""
-- fallback set before nest
GET /a/x 200 "outer_fallback sees /a/x"
-- method_not_allowed_fallback before / after nest
POST /a/users 405 ""
POST /a/users 405 "custom 405 for POST"
-- merge keeps whichever side has the fallback
GET /x 200 "left_fallback sees /x"
GET /x 200 "right_fallback sees /x"
GET /b/ 由父路由器的 fallback 处理,看到的是完整的 /b/:
- 父路由器的 fallback 树里有四条:
/、/{*__private__axum_fallback}(父的),/b、/b/{*__private__axum_fallback}(从子路由器搬来的)。 /b/不等于/b;/b/{*..}的通配不匹配空,所以子路由器那两条都不命中。- 剩下的
/{*..}匹配b/,父路由器的 fallback 接住。它没有套StripPrefix,URI 不变。
明明是「/b 下面的请求」,却绕过了 /b 自己的 fallback。如果子路由器的 fallback 回的是特定格式(比如 JSON 错误体),/b/ 会得到父路由器的格式。
小结
nest把子路由器拆开:每条路由加前缀、套StripPrefix和SetNestedPath,逐条注册进父路由器;子路由的/变成前缀本身,所以/prefix/不匹配。nest_service只注册{prefix}、{prefix}/、{prefix}/{*tail}三条,服务收到剥过前缀的请求;Router交给它会多匹配一次,且不继承 fallback。- 在根上 nest、前缀含通配会 panic;同一前缀 nest 两次可以,只要路由不撞。
merge逐条重新注册,冲突规则和route相同;两边都有自定义 fallback 时 panic,先reset_fallback其中一个。- fallback 是第二棵路由树里的
/和/{*..}两条路由;nest把子路由器的 fallback 树加前缀搬过来,靠静态优先实现「子覆盖父」,子路由器没有 fallback 时自然落到父的。 - 405 不走
Router::fallback;method_not_allowed_fallback只作用于调用时已经存在的路由。