Rust in Everything
基于 topcoat-router 0.10.0

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,编译器和启动日志都不提示。

💬 关联讨论

发起讨论
加载中...