module path must be under module router's root
主线位置:发现主线的最后一步。
module_router!()在启动时取出 inventory 里所有的模块派生 handler,把各自的module_path!()减去根模块,切成 URL(模块路由与发现)。减不掉的时候就是本页的 panic。
flowchart TB
inv["inventory:整个二进制里的 #[page]"] --> loop["discover_pages 逐个处理"]
loop --> rel{"module_path 以根开头"}
rel -->|是| url["切段成 URL,注册"]
rel -->|否| panic["expect 失败:启动 panic"]
nomod["没有 mod 声明的文件"] -.->|"不编译、不登记"| missing["URL 静默 404"]
现象一:启动就 panic
module_router!() 写在 mod app 里,crate 根还留着一个 #[page](依赖:topcoat =0.10.0 的 full、tokio =1.54.1):
// main.rs
use topcoat::{Result, router::page, view::{View, view}};
mod app;
#[tokio::main]
async fn main() {
topcoat::start(app::router()).await.unwrap();
}
#[page]
async fn health() -> Result<impl View> {
Ok(view! { "ok" })
}
// app.rs
mod docs;
pub fn router() -> Router {
module_router!().build()
}
thread 'main' (2645349) panicked at .../topcoat-router-0.10.0/src/module/router.rs:84:29:
module path must be under module router's root
进程退出码 101,端口都没绑上。
原因
ModuleRouterBuilder 的 discover_* 遍历的是 inventory 里所有登记项,不分它们属于哪棵模块树。对每一项调 relative_module_path:
fn relative_module_path(&self, module_path: &'static str) -> &'static str {
// ...
.expect("module path must be under module router's root")
}
health 的模块路径是 crate 根(比如二进制叫 myapp,就是 myapp),根模块是 myapp::app,前者不以后者开头,expect 失败。panic 位置指向第 84 行,是因为 relative_module_path 标了 #[track_caller],报的是调用它的那一行。
同一个原因还有两种来法:
- 依赖的某个库里用了
#[page]、#[layout]这类不写路径的属性(例如一个组件库顺手带了一个演示页面)。 - 一个二进制里想放两个
module_router!(),分别管两棵树。
这些在 0.10.0 都会让启动 panic。模块路由的文档说的是只收「module containing module_router!()」之下的 handler,代码比文档宽。tag 之后的 #518 给每个 discover_* 加了过滤,只收根之下的。
修法(0.10.0)
- 把所有模块派生的 handler 都放到
module_router!()所在模块之下。上例把health挪进app里,URL 是/;想要/health就挪进app::health模块。 - 确实要放在根外面的,写显式的绝对路径:
#[page("/health")]。绝对路径的 handler 不算模块派生,module_router!()不收它,要.discover()才注册(模块路由与发现的.discover()一节)。 - 第三方库里的模块派生 handler 绕不开,只能升级到包含 #518 的版本,或者不用
module_router!()、改为显式路径加.discover()。
现象二:页面 404,什么提示都没有
反方向的问题在 0.10.0 和之后都一样。app.rs 里少写了一行 mod blog;,app/blog.rs 文件明明在:
// app.rs
mod docs;
// mod blog; // 忘了
### GET /docs
HTTP/1.1 200 OK
docs
### GET /blog
HTTP/1.1 404 Not Found
not found
服务器启动时什么也不打印,cargo build 也没有任何警告:没有被 mod 声明的文件根本不参与编译,rustc 不会去看它,inventory 里也就没有它。文件系统里的目录结构和路由表之间,唯一的连接是 mod 声明。
排查
- 启动 panic 带着「module router's root」:在整个工作区和依赖里找不写路径的
#[page]、#[layout]、#[layer]、#[route],看哪个不在根之下。 - 页面 404:先确认文件从
main.rs/lib.rs一路都有mod声明。一个快速的办法是在那个文件里故意写一个语法错误再编译:编译照样通过,就说明它没有被编译。 - 0.10.0 的 CLI 没有列出路由表的命令。对关键页面,在用到它的地方写
href!(blog::page)生成链接:href!的目标是 Rust 路径,模块没声明时这一行编译不过,静默 404 就变成了编译错误,rustc 还会提示缺的那行mod(文件路径按普通项目的布局改写过):
error[E0433]: cannot find module or crate `blog` in this scope
--> src/app.rs:11:26
|
11 | topcoat::router::href!(blog::page).to_string()
| ^^^^ use of unresolved module or unlinked crate `blog`
|
help: to make use of source file src/app/blog.rs, use `mod blog` in this file to declare the module
再保险一点,在测试里把关键 URL 请求一遍。
小结
- 0.10.0 的
module_router!()收整个二进制里的模块派生 handler,根之外有一个就在启动时 panic「module path must be under module router's root」;#518 之后只收根之下的。 - 修法:把 handler 挪到根之下,或者写绝对路径并
.discover()。 - 没有
mod声明的文件不编译、不登记,URL 静默 404,编译器和启动日志都不提示。