news 2026/9/14 12:19:39

axum 嵌套路由会剥离前缀,如何用 OriginalUri 取回原始 URI

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
axum 嵌套路由会剥离前缀,如何用 OriginalUri 取回原始 URI

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只得到/bazRequest上的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.0OriginalUri内部的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 前后行为的对照。

边界与限制

  • 只有经过 axumRouter的请求才会自动带上OriginalUri扩展。文档的 middleware 示例中对"没有扩展"的情况做了回退处理,说明脱离Router体系(或扩展被移除)时读不到原始 URI。
  • OriginalUri本身没有拒绝类型(Rejection = Infallible),不存在提取失败导致的响应分支需要处理。
  • 前缀剥离是nest的固有语义,OriginalUri只负责取回原始值,不会改变嵌套路由的匹配行为;nest与通配符路由的匹配差异(例如/foo/{*rest}不匹配/foo本身,而嵌套在/fooRouter匹配/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 12:17:38

Vue3+Element-Plus图书管理系统源码拆解:从登录到部署全流程

简介&#xff1a;基于Vue3与Element-Plus构建的图书管理系统设计源码&#xff0c;面向需要快速搭建图书管理功能的前端学习者和初级工程师&#xff0c;可支撑图书信息维护、借阅归还、读者管理等常见业务场景。压缩包共33个文件&#xff0c;主要由14个Vue组件、10个JavaScript脚…

作者头像 李华
网站建设 2026/9/14 12:16:30

网页游戏源码合集高效利用:分类、本地运行与改造指南

简介&#xff1a;这份网页游戏源码合集以HTML、JS、CSS为主&#xff0c;集中了植物大战僵尸、黄金矿工、扫雷、开心消消乐等数十款经典网页游戏的完整前端实现&#xff0c;适合前端初学者、游戏开发爱好者及需要网页互动案例的课件制作者参考。压缩包共2003个文件&#xff0c;其…

作者头像 李华
网站建设 2026/9/14 12:13:07

C语言学生奖学金管理系统:从结构体设计到文件读写完整课设指南

简介&#xff1a;基于C语言实现的学生奖学金管理系统&#xff0c;是一份面向C语言初学者、高校课程设计及毕业设计学生的完整实践资源&#xff0c;可有效解决课程设计中缺少可运行项目参考的问题。系统围绕学生信息与奖学金管理&#xff0c;覆盖结构体定义、链表或数组动态管理…

作者头像 李华
网站建设 2026/9/14 12:12:25

5分钟完成 Klipper 容器化部署:镜像构建到串口挂载的完整指南

5分钟完成 Klipper 容器化部署&#xff1a;镜像构建到串口挂载的完整指南 【免费下载链接】klipper Klipper is a 3d-printer firmware 项目地址: https://gitcode.com/GitHub_Trending/kl/klipper Klipper 是一套 3D 打印机固件&#xff0c;把运动规划交给普通电脑&…

作者头像 李华