axum/extract/
matched_path.rs

1use super::{rejection::*, FromRequestParts};
2use crate::routing::{RouteId, NEST_TAIL_PARAM_CAPTURE};
3use axum_core::extract::OptionalFromRequestParts;
4use http::request::Parts;
5use std::{collections::HashMap, convert::Infallible, sync::Arc};
6
7/// Access the path in the router that matches the request.
8///
9/// ```
10/// use axum::{
11///     Router,
12///     extract::MatchedPath,
13///     routing::get,
14/// };
15///
16/// let app = Router::new().route(
17///     "/users/{id}",
18///     get(|path: MatchedPath| async move {
19///         let path = path.as_str();
20///         // `path` will be "/users/{id}"
21///     })
22/// );
23/// # let _: Router = app;
24/// ```
25///
26/// # Accessing `MatchedPath` via extensions
27///
28/// `MatchedPath` can also be accessed from middleware via request extensions.
29///
30/// This is useful for example with [`Trace`](tower_http::trace::Trace) to
31/// create a span that contains the matched path:
32///
33/// ```
34/// use axum::{
35///     Router,
36///     extract::{Request, MatchedPath},
37///     routing::get,
38/// };
39/// use tower_http::trace::TraceLayer;
40///
41/// let app = Router::new()
42///     .route("/users/{id}", get(|| async { /* ... */ }))
43///     .layer(
44///         TraceLayer::new_for_http().make_span_with(|req: &Request<_>| {
45///             let path = if let Some(path) = req.extensions().get::<MatchedPath>() {
46///                 path.as_str()
47///             } else {
48///                 req.uri().path()
49///             };
50///             tracing::info_span!("http-request", %path)
51///         }),
52///     );
53/// # let _: Router = app;
54/// ```
55#[cfg_attr(docsrs, doc(cfg(feature = "matched-path")))]
56#[derive(Clone, Debug)]
57pub struct MatchedPath(pub(crate) Arc<str>);
58
59impl MatchedPath {
60    /// Returns a `str` representation of the path.
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65}
66
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
84impl<S> OptionalFromRequestParts<S> for MatchedPath
85where
86    S: Send + Sync,
87{
88    type Rejection = Infallible;
89
90    async fn from_request_parts(
91        parts: &mut Parts,
92        _state: &S,
93    ) -> Result<Option<Self>, Self::Rejection> {
94        Ok(parts.extensions.get::<Self>().cloned())
95    }
96}
97
98#[derive(Clone, Debug)]
99struct MatchedNestedPath(Arc<str>);
100
101pub(crate) fn set_matched_path_for_request(
102    id: RouteId,
103    route_id_to_path: &HashMap<RouteId, Arc<str>>,
104    extensions: &mut http::Extensions,
105) {
106    let matched_path = if let Some(matched_path) = route_id_to_path.get(&id) {
107        matched_path
108    } else {
109        #[cfg(debug_assertions)]
110        panic!("should always have a matched path for a route id");
111        #[cfg(not(debug_assertions))]
112        return;
113    };
114
115    let matched_path = append_nested_matched_path(matched_path, extensions);
116
117    if matched_path.ends_with(NEST_TAIL_PARAM_CAPTURE) {
118        extensions.insert(MatchedNestedPath(matched_path));
119        debug_assert!(extensions.remove::<MatchedPath>().is_none());
120    } else {
121        extensions.insert(MatchedPath(matched_path));
122        extensions.remove::<MatchedNestedPath>();
123    }
124}
125
126// a previous `MatchedPath` might exist if we're inside a nested Router
127fn append_nested_matched_path(matched_path: &Arc<str>, extensions: &http::Extensions) -> Arc<str> {
128    if let Some(previous) = extensions
129        .get::<MatchedPath>()
130        .map(|matched_path| matched_path.as_str())
131        .or_else(|| Some(&extensions.get::<MatchedNestedPath>()?.0))
132    {
133        let previous = previous
134            .strip_suffix(NEST_TAIL_PARAM_CAPTURE)
135            .unwrap_or(previous);
136
137        let matched_path = format!("{previous}{matched_path}");
138        matched_path.into()
139    } else {
140        Arc::clone(matched_path)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::{
148        extract::Request,
149        handler::HandlerWithoutStateExt,
150        middleware::map_request,
151        routing::{any, get},
152        test_helpers::*,
153        Router,
154    };
155    use http::StatusCode;
156
157    #[crate::test]
158    async fn extracting_on_handler() {
159        let app = Router::new().route(
160            "/{a}",
161            get(|path: MatchedPath| async move { path.as_str().to_owned() }),
162        );
163
164        let client = TestClient::new(app);
165
166        let res = client.get("/foo").await;
167        assert_eq!(res.text().await, "/{a}");
168    }
169
170    #[crate::test]
171    async fn extracting_on_handler_in_nested_router() {
172        let app = Router::new().nest(
173            "/{a}",
174            Router::new().route(
175                "/{b}",
176                get(|path: MatchedPath| async move { path.as_str().to_owned() }),
177            ),
178        );
179
180        let client = TestClient::new(app);
181
182        let res = client.get("/foo/bar").await;
183        assert_eq!(res.text().await, "/{a}/{b}");
184    }
185
186    #[crate::test]
187    async fn extracting_on_handler_in_deeply_nested_router() {
188        let app = Router::new().nest(
189            "/{a}",
190            Router::new().nest(
191                "/{b}",
192                Router::new().route(
193                    "/{c}",
194                    get(|path: MatchedPath| async move { path.as_str().to_owned() }),
195                ),
196            ),
197        );
198
199        let client = TestClient::new(app);
200
201        let res = client.get("/foo/bar/baz").await;
202        assert_eq!(res.text().await, "/{a}/{b}/{c}");
203    }
204
205    #[crate::test]
206    async fn cannot_extract_nested_matched_path_in_middleware() {
207        async fn extract_matched_path<B>(
208            matched_path: Option<MatchedPath>,
209            req: Request<B>,
210        ) -> Request<B> {
211            assert!(matched_path.is_none());
212            req
213        }
214
215        let app = Router::new()
216            .nest_service("/{a}", Router::new().route("/{b}", get(|| async move {})))
217            .layer(map_request(extract_matched_path));
218
219        let client = TestClient::new(app);
220
221        let res = client.get("/foo/bar").await;
222        assert_eq!(res.status(), StatusCode::OK);
223    }
224
225    #[crate::test]
226    async fn can_extract_nested_matched_path_in_middleware_using_nest() {
227        async fn extract_matched_path<B>(
228            matched_path: Option<MatchedPath>,
229            req: Request<B>,
230        ) -> Request<B> {
231            assert_eq!(matched_path.unwrap().as_str(), "/{a}/{b}");
232            req
233        }
234
235        let app = Router::new()
236            .nest("/{a}", Router::new().route("/{b}", get(|| async move {})))
237            .layer(map_request(extract_matched_path));
238
239        let client = TestClient::new(app);
240
241        let res = client.get("/foo/bar").await;
242        assert_eq!(res.status(), StatusCode::OK);
243    }
244
245    #[crate::test]
246    async fn cannot_extract_nested_matched_path_in_middleware_via_extension() {
247        async fn assert_no_matched_path<B>(req: Request<B>) -> Request<B> {
248            assert!(req.extensions().get::<MatchedPath>().is_none());
249            req
250        }
251
252        let app = Router::new()
253            .nest_service("/{a}", Router::new().route("/{b}", get(|| async move {})))
254            .layer(map_request(assert_no_matched_path));
255
256        let client = TestClient::new(app);
257
258        let res = client.get("/foo/bar").await;
259        assert_eq!(res.status(), StatusCode::OK);
260    }
261
262    #[tokio::test]
263    async fn can_extract_nested_matched_path_in_middleware_via_extension_using_nest() {
264        async fn assert_matched_path<B>(req: Request<B>) -> Request<B> {
265            assert!(req.extensions().get::<MatchedPath>().is_some());
266            req
267        }
268
269        let app = Router::new()
270            .nest("/{a}", Router::new().route("/{b}", get(|| async move {})))
271            .layer(map_request(assert_matched_path));
272
273        let client = TestClient::new(app);
274
275        let res = client.get("/foo/bar").await;
276        assert_eq!(res.status(), StatusCode::OK);
277    }
278
279    #[crate::test]
280    async fn can_extract_nested_matched_path_in_middleware_on_nested_router() {
281        async fn extract_matched_path<B>(matched_path: MatchedPath, req: Request<B>) -> Request<B> {
282            assert_eq!(matched_path.as_str(), "/{a}/{b}");
283            req
284        }
285
286        let app = Router::new().nest(
287            "/{a}",
288            Router::new()
289                .route("/{b}", get(|| async move {}))
290                .layer(map_request(extract_matched_path)),
291        );
292
293        let client = TestClient::new(app);
294
295        let res = client.get("/foo/bar").await;
296        assert_eq!(res.status(), StatusCode::OK);
297    }
298
299    #[crate::test]
300    async fn can_extract_nested_matched_path_in_middleware_on_nested_router_via_extension() {
301        async fn extract_matched_path<B>(req: Request<B>) -> Request<B> {
302            let matched_path = req.extensions().get::<MatchedPath>().unwrap();
303            assert_eq!(matched_path.as_str(), "/{a}/{b}");
304            req
305        }
306
307        let app = Router::new().nest(
308            "/{a}",
309            Router::new()
310                .route("/{b}", get(|| async move {}))
311                .layer(map_request(extract_matched_path)),
312        );
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]
321    async fn extracting_on_nested_handler() {
322        async fn handler(path: Option<MatchedPath>) {
323            assert!(path.is_none());
324        }
325
326        let app = Router::new().nest_service("/{a}", handler.into_service());
327
328        let client = TestClient::new(app);
329
330        let res = client.get("/foo/bar").await;
331        assert_eq!(res.status(), StatusCode::OK);
332    }
333
334    // https://github.com/tokio-rs/axum/issues/1579
335    #[crate::test]
336    async fn doesnt_panic_if_router_called_from_wildcard_route() {
337        use tower::ServiceExt;
338
339        let app = Router::new().route(
340            "/{*path}",
341            any(|req: Request| {
342                Router::new()
343                    .nest("/foo", Router::new().route("/bar", get(|| async {})))
344                    .oneshot(req)
345            }),
346        );
347
348        let client = TestClient::new(app);
349
350        let res = client.get("/foo/bar").await;
351        assert_eq!(res.status(), StatusCode::OK);
352    }
353
354    #[crate::test]
355    async fn cant_extract_in_fallback() {
356        async fn handler(path: Option<MatchedPath>, req: Request) {
357            assert!(path.is_none());
358            assert!(req.extensions().get::<MatchedPath>().is_none());
359        }
360
361        let app = Router::new().fallback(handler);
362
363        let client = TestClient::new(app);
364
365        let res = client.get("/foo/bar").await;
366        assert_eq!(res.status(), StatusCode::OK);
367    }
368
369    #[crate::test]
370    async fn matching_colon() {
371        let app = Router::new().without_v07_checks().route(
372            "/:foo",
373            get(|path: MatchedPath| async move { path.as_str().to_owned() }),
374        );
375
376        let client = TestClient::new(app);
377
378        let res = client.get("/:foo").await;
379        assert_eq!(res.status(), StatusCode::OK);
380        assert_eq!(res.text().await, "/:foo");
381
382        let res = client.get("/:bar").await;
383        assert_eq!(res.status(), StatusCode::NOT_FOUND);
384
385        let res = client.get("/foo").await;
386        assert_eq!(res.status(), StatusCode::NOT_FOUND);
387    }
388
389    #[crate::test]
390    async fn matching_asterisk() {
391        let app = Router::new().without_v07_checks().route(
392            "/*foo",
393            get(|path: MatchedPath| async move { path.as_str().to_owned() }),
394        );
395
396        let client = TestClient::new(app);
397
398        let res = client.get("/*foo").await;
399        assert_eq!(res.status(), StatusCode::OK);
400        assert_eq!(res.text().await, "/*foo");
401
402        let res = client.get("/*bar").await;
403        assert_eq!(res.status(), StatusCode::NOT_FOUND);
404
405        let res = client.get("/foo").await;
406        assert_eq!(res.status(), StatusCode::NOT_FOUND);
407    }
408}