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#[cfg_attr(docsrs, doc(cfg(feature = "matched-path")))]
56#[derive(Clone, Debug)]
57pub struct MatchedPath(pub(crate) Arc<str>);
58
59impl MatchedPath {
60 #[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
126fn 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 #[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}