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/// 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    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
112// a previous `MatchedPath` might exist if we're inside a nested Router
113fn 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    // https://github.com/tokio-rs/axum/issues/1579
321    #[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}