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