axum 嵌套路由会剥离前缀,如何用 OriginalUri 取回原始 URI
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
用 axum 的nest把多个Router组合起来后,你会遇到一个不直观的行为:嵌套层里的 handler 提取到Uri(或调用req.uri())时,看到的是剥掉了嵌套前缀的路径,而不是客户端实际请求的完整 URI。nest.md文档直接说明了这一点:嵌套路由看到的不是原始请求 URI,而是"have the matched prefix stripped"(匹配到的前缀被剥离),这样做是必要的,否则静态文件服务等依赖路径前缀的服务无法工作。如果你需要拿到原始请求 URI(例如记录完整访问路径、做重定向或鉴权),文档给出的答案是使用OriginalUri提取器。
先确认你遇到的是剥离行为,而不是路由没匹配上
仓库测试 axum/src/routing/tests/nest.rs 中有两组对照测试,可以直接用来判断Uri提取器在嵌套层里看到的内容:
nested_url_extractor测试把三级Router分别嵌套在/foo、/bar下,内部路由是/baz,handler 提取Uri并把uri.to_string()作为响应体:
let app = Router::new().nest( "/foo", Router::new().nest( "/bar", Router::new() .route("/baz", get(|uri: Uri| async move { uri.to_string() })) .route( "/qux", get(|req: Request| async move { req.uri().to_string() }), ), ), ); let client = TestClient::new(app); let res = client.get("/foo/bar/baz").await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.text().await, "/baz"); let res = client.get("/foo/bar/qux").await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.text().await, "/qux");文档中的断言结果是:请求/foo/bar/baz时,handler 里的Uri只得到/baz;Request上的req.uri()同样只有/qux。这是 axum 的既有设计,不是 bug。Uri提取器本身的实现就是从请求 parts 里原样克隆当前 URI(见 axum-core/src/extract/request_parts.rs 中impl FromRequestParts for Uri),而嵌套服务拿到时 parts 中的 URI 已经是剥离后的值。
另外注意与通配符路由的区别:nest.md 指出,通配符路由(如/foo/{*rest})仍然能看到完整 URI,而嵌套路由会被剥离前缀。如果你的场景只是想"看到完整路径"且不需要前缀剥离语义,也可以考虑用通配符路由替代nest。
在 handler 中用 OriginalUri 提取原始 URI
OriginalUri的定义和文档在 axum/src/extract/original_uri.rs:
pub struct OriginalUri(pub Uri);它是一个FromRequestParts提取器,Rejection类型为Infallible——只要请求进入了 axum 的路由体系,这个提取就永远不会失败。
使用OriginalUri依赖original-urifeature。根据 axum/Cargo.toml,该 feature 已包含在默认 feature 列表里,正常cargo add axum引入的依赖无需额外配置;只有在自行裁剪 feature 时才需要显式开启。
文档给出的标准用法是把OriginalUri作为 handler 参数,和Uri并排对比:
use axum::{ routing::get, Router, extract::OriginalUri, http::Uri }; let api_routes = Router::new() .route( "/users", get(|uri: Uri, OriginalUri(original_uri): OriginalUri| async { // `uri` is `/users` // `original_uri` is `/api/users` }), ); let app = Router::new().nest("/api", api_routes);这段文档示例对应的事实是:嵌套在/api下的/users路由,Uri参数得到/users,而OriginalUri得到客户端真实请求的/api/users。多套几层nest时同理——OriginalUri始终是最外层收到的完整路径。
它从哪里拿到原始 URI
提取器的实现(original_uri.rs 中的from_request_parts)是:先尝试从请求扩展里取Extension::<OriginalUri>,取不到时才退回当前parts.uri.clone()。而 axum/src/routing/path_router.rs 中的路由处理逻辑会在original-urifeature 开启时,于请求进入路由前检查扩展里是否已有OriginalUri,没有就插入一份当时的req.uri()。也就是说,只要请求经过顶层Router,扩展里就已经存好了完整 URI,深层嵌套路由的 handler 再提取时拿到的自然是未剥离的值。
在 middleware 或 trace 日志中通过请求扩展取回
handler 之外,OriginalUri还能从请求扩展直接读取。文档给出的场景是配合tower_http::trace::TraceLayer创建包含完整路径的 span(你的服务可能会被嵌套,此时req.uri()只有剥离后的路径):
use axum::{ Router, extract::OriginalUri, http::Request, routing::get, }; use tower_http::trace::TraceLayer; let api_routes = Router::new() .route("/users/{id}", get(|| async { /* ... */ })) .layer( TraceLayer::new_for_http().make_span_with(|req: &Request<_>| { let path = if let Some(path) = req.extensions().get::<OriginalUri>() { // This will include `/api` path.0.path().to_owned() } else { // The `OriginalUri` extension will always be present if using // `Router` unless another extractor or middleware has removed it req.uri().path().to_owned() }; tracing::info_span!("http-request", %path) }), ); let app = Router::new().nest("/api", api_routes);关键点:path.0是OriginalUri内部的Uri,.path()取路径部分。文档明确说明,只要使用Router构建服务,OriginalUri扩展"will always be present"(总是存在),除非其他提取器或 middleware 把它移除了——所以示例中保留了else分支回退到req.uri().path()。仓库自带的日志示例 examples/tracing-aka-logging/src/main.rs 也是同一思路,它用MatchedPath记录匹配到的路由模板,并在注释中提示"Use request.uri() or OriginalUri if you want the real path"。
用仓库测试验证取回的值
验证方式可以直接照搬 axum/src/routing/tests/nest.rs 中的nested_url_original_extractor测试,它断言了OriginalUri在两层嵌套下的取回结果:
let app = Router::new().nest( "/foo", Router::new().nest( "/bar", Router::new().route( "/baz", get(|uri: extract::OriginalUri| async move { uri.0.to_string() }), ), ), ); let client = TestClient::new(app); let res = client.get("/foo/bar/baz").await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.text().await, "/foo/bar/baz");按文档中的断言,GET /foo/bar/baz的响应体应当是/foo/bar/baz——完整 URI 被原样取回,而前面nested_url_extractor测试里同样的请求在Uri提取器下只得到/baz。你可以把这两段代码放进自己的工程用TestClient跑一遍(TestClient是 axum 的测试辅助工具,dev-dependency 中自带),作为改造 handler 前后行为的对照。
边界与限制
- 只有经过 axum
Router的请求才会自动带上OriginalUri扩展。文档的 middleware 示例中对"没有扩展"的情况做了回退处理,说明脱离Router体系(或扩展被移除)时读不到原始 URI。 OriginalUri本身没有拒绝类型(Rejection = Infallible),不存在提取失败导致的响应分支需要处理。- 前缀剥离是
nest的固有语义,OriginalUri只负责取回原始值,不会改变嵌套路由的匹配行为;nest与通配符路由的匹配差异(例如/foo/{*rest}不匹配/foo本身,而嵌套在/foo的Router匹配/foo但不匹配/foo/)以 nest.md 的说明为准。
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考