axum/extract/
matched_path.rs
1use super::{rejection::*, FromRequestParts};
2use crate::routing::{RouteId, NEST_TAIL_PARAM_CAPTURE};
3use async_trait::async_trait;
4use http::request::Parts;
5use std::{collections::HashMap, sync::Arc};
6
7#[cfg_attr(docsrs, doc(cfg(feature = "matched-path")))]
56#[derive(Clone, Debug)]
57pub struct MatchedPath(pub(crate) Arc<str>);
58
59impl MatchedPath {
60 pub fn as_str(&self) -> &str {
62 &self.0
63 }
64}
65
66#[async_trait]
67impl<S> FromRequestParts<S> for MatchedPath
68where
69 S: Send + Sync,
70{
71 type Rejection = MatchedPathRejection;
72
73 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
74 let matched_path = parts
75 .extensions
76 .get::<Self>()
77 .ok_or(MatchedPathRejection::MatchedPathMissing(MatchedPathMissing))?
78 .clone();
79
80 Ok(matched_path)
81 }
82}
83
84#[derive(Clone, Debug)]
85struct MatchedNestedPath(Arc<str>);
86
87pub(crate) fn set_matched_path_for_request(
88 id: RouteId,
89 route_id_to_path: &HashMap<RouteId, Arc<str>>,
90 extensions: &mut http::Extensions,
91) {
92 let matched_path = if let Some(matched_path) = route_id_to_path.get(&id) {
93 matched_path
94 } else {
95 #[cfg(debug_assertions)]
96 panic!("should always have a matched path for a route id");
97 #[cfg(not(debug_assertions))]
98 return;
99 };
100
101 let matched_path = append_nested_matched_path(matched_path, extensions);
102
103 if matched_path.ends_with(NEST_TAIL_PARAM_CAPTURE) {
104 extensions.insert(MatchedNestedPath(matched_path));
105 debug_assert!(extensions.remove::<MatchedPath>().is_none());
106 } else {
107 extensions.insert(MatchedPath(matched_path));
108 extensions.remove::<MatchedNestedPath>();
109 }
110}
111
112fn append_nested_matched_path(matched_path: &Arc<str>, extensions: &http::Extensions) -> Arc<str> {
114 if let Some(previous) = extensions
115 .get::<MatchedPath>()
116 .map(|matched_path| matched_path.as_str())
117 .or_else(|| Some(&extensions.get::<MatchedNestedPath>()?.0))
118 {
119 let previous = previous
120 .strip_suffix(NEST_TAIL_PARAM_CAPTURE)
121 .unwrap_or(previous);
122
123 let matched_path = format!("{previous}{matched_path}");
124 matched_path.into()
125 } else {
126 Arc::clone(matched_path)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::{
134 extract::Request,
135 handler::HandlerWithoutStateExt,
136 middleware::map_request,
137 routing::{any, get},
138 test_helpers::*,
139 Router,
140 };
141 use http::StatusCode;
142
143 #[crate::test]
144 async fn extracting_on_handler() {
145 let app = Router::new().route(
146 "/:a",
147 get(|path: MatchedPath| async move { path.as_str().to_owned() }),
148 );
149
150 let client = TestClient::new(app);
151
152 let res = client.get("/foo").await;
153 assert_eq!(res.text().await, "/:a");
154 }
155
156 #[crate::test]
157 async fn extracting_on_handler_in_nested_router() {
158 let app = Router::new().nest(
159 "/:a",
160 Router::new().route(
161 "/:b",
162 get(|path: MatchedPath| async move { path.as_str().to_owned() }),
163 ),
164 );
165
166 let client = TestClient::new(app);
167
168 let res = client.get("/foo/bar").await;
169 assert_eq!(res.text().await, "/:a/:b");
170 }
171
172 #[crate::test]
173 async fn extracting_on_handler_in_deeply_nested_router() {
174 let app = Router::new().nest(
175 "/:a",
176 Router::new().nest(
177 "/:b",
178 Router::new().route(
179 "/:c",
180 get(|path: MatchedPath| async move { path.as_str().to_owned() }),
181 ),
182 ),
183 );
184
185 let client = TestClient::new(app);
186
187 let res = client.get("/foo/bar/baz").await;
188 assert_eq!(res.text().await, "/:a/:b/:c");
189 }
190
191 #[crate::test]
192 async fn cannot_extract_nested_matched_path_in_middleware() {
193 async fn extract_matched_path<B>(
194 matched_path: Option<MatchedPath>,
195 req: Request<B>,
196 ) -> Request<B> {
197 assert!(matched_path.is_none());
198 req
199 }
200
201 let app = Router::new()
202 .nest_service("/:a", Router::new().route("/:b", get(|| async move {})))
203 .layer(map_request(extract_matched_path));
204
205 let client = TestClient::new(app);
206
207 let res = client.get("/foo/bar").await;
208 assert_eq!(res.status(), StatusCode::OK);
209 }
210
211 #[crate::test]
212 async fn can_extract_nested_matched_path_in_middleware_using_nest() {
213 async fn extract_matched_path<B>(
214 matched_path: Option<MatchedPath>,
215 req: Request<B>,
216 ) -> Request<B> {
217 assert_eq!(matched_path.unwrap().as_str(), "/:a/:b");
218 req
219 }
220
221 let app = Router::new()
222 .nest("/:a", Router::new().route("/:b", get(|| async move {})))
223 .layer(map_request(extract_matched_path));
224
225 let client = TestClient::new(app);
226
227 let res = client.get("/foo/bar").await;
228 assert_eq!(res.status(), StatusCode::OK);
229 }
230
231 #[crate::test]
232 async fn cannot_extract_nested_matched_path_in_middleware_via_extension() {
233 async fn assert_no_matched_path<B>(req: Request<B>) -> Request<B> {
234 assert!(req.extensions().get::<MatchedPath>().is_none());
235 req
236 }
237
238 let app = Router::new()
239 .nest_service("/:a", Router::new().route("/:b", get(|| async move {})))
240 .layer(map_request(assert_no_matched_path));
241
242 let client = TestClient::new(app);
243
244 let res = client.get("/foo/bar").await;
245 assert_eq!(res.status(), StatusCode::OK);
246 }
247
248 #[tokio::test]
249 async fn can_extract_nested_matched_path_in_middleware_via_extension_using_nest() {
250 async fn assert_matched_path<B>(req: Request<B>) -> Request<B> {
251 assert!(req.extensions().get::<MatchedPath>().is_some());
252 req
253 }
254
255 let app = Router::new()
256 .nest("/:a", Router::new().route("/:b", get(|| async move {})))
257 .layer(map_request(assert_matched_path));
258
259 let client = TestClient::new(app);
260
261 let res = client.get("/foo/bar").await;
262 assert_eq!(res.status(), StatusCode::OK);
263 }
264
265 #[crate::test]
266 async fn can_extract_nested_matched_path_in_middleware_on_nested_router() {
267 async fn extract_matched_path<B>(matched_path: MatchedPath, req: Request<B>) -> Request<B> {
268 assert_eq!(matched_path.as_str(), "/:a/:b");
269 req
270 }
271
272 let app = Router::new().nest(
273 "/:a",
274 Router::new()
275 .route("/:b", get(|| async move {}))
276 .layer(map_request(extract_matched_path)),
277 );
278
279 let client = TestClient::new(app);
280
281 let res = client.get("/foo/bar").await;
282 assert_eq!(res.status(), StatusCode::OK);
283 }
284
285 #[crate::test]
286 async fn can_extract_nested_matched_path_in_middleware_on_nested_router_via_extension() {
287 async fn extract_matched_path<B>(req: Request<B>) -> Request<B> {
288 let matched_path = req.extensions().get::<MatchedPath>().unwrap();
289 assert_eq!(matched_path.as_str(), "/:a/:b");
290 req
291 }
292
293 let app = Router::new().nest(
294 "/:a",
295 Router::new()
296 .route("/:b", get(|| async move {}))
297 .layer(map_request(extract_matched_path)),
298 );
299
300 let client = TestClient::new(app);
301
302 let res = client.get("/foo/bar").await;
303 assert_eq!(res.status(), StatusCode::OK);
304 }
305
306 #[crate::test]
307 async fn extracting_on_nested_handler() {
308 async fn handler(path: Option<MatchedPath>) {
309 assert!(path.is_none());
310 }
311
312 let app = Router::new().nest_service("/:a", handler.into_service());
313
314 let client = TestClient::new(app);
315
316 let res = client.get("/foo/bar").await;
317 assert_eq!(res.status(), StatusCode::OK);
318 }
319
320 #[crate::test]
322 async fn doesnt_panic_if_router_called_from_wildcard_route() {
323 use tower::ServiceExt;
324
325 let app = Router::new().route(
326 "/*path",
327 any(|req: Request| {
328 Router::new()
329 .nest("/", Router::new().route("/foo", get(|| async {})))
330 .oneshot(req)
331 }),
332 );
333
334 let client = TestClient::new(app);
335
336 let res = client.get("/foo").await;
337 assert_eq!(res.status(), StatusCode::OK);
338 }
339
340 #[crate::test]
341 async fn cant_extract_in_fallback() {
342 async fn handler(path: Option<MatchedPath>, req: Request) {
343 assert!(path.is_none());
344 assert!(req.extensions().get::<MatchedPath>().is_none());
345 }
346
347 let app = Router::new().fallback(handler);
348
349 let client = TestClient::new(app);
350
351 let res = client.get("/foo/bar").await;
352 assert_eq!(res.status(), StatusCode::OK);
353 }
354}