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 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
125fn 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 #[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}