Skip to main content

actix_web/types/
path.rs

1//! For path segment extractor documentation, see [`Path`].
2
3use std::sync::Arc;
4
5use actix_router::PathDeserializer;
6use actix_utils::future::{ready, Ready};
7use derive_more::{AsRef, Deref, DerefMut, Display, From};
8use serde::de;
9
10use crate::{
11    dev::Payload,
12    error::{Error, ErrorNotFound, PathError},
13    web::Data,
14    FromRequest, HttpRequest,
15};
16
17/// Extract typed data from request path segments.
18///
19/// Use [`PathConfig`] to configure extraction option.
20///
21/// Unlike, [`HttpRequest::match_info`], this extractor will fully percent-decode dynamic segments,
22/// including `/`, `%`, and `+`.
23///
24/// # Examples
25/// ```
26/// use actix_web::{get, web};
27///
28/// // extract path info from "/{name}/{count}/index.html" into tuple
29/// // {name}  - deserialize a String
30/// // {count} - deserialize a u32
31/// #[get("/{name}/{count}/index.html")]
32/// async fn index(path: web::Path<(String, u32)>) -> String {
33///     let (name, count) = path.into_inner();
34///     format!("Welcome {}! {}", name, count)
35/// }
36/// ```
37///
38/// Path segments also can be deserialized into any type that implements [`serde::Deserialize`].
39/// Path segment labels will be matched with struct field names.
40///
41/// ```
42/// use actix_web::{get, web};
43/// use serde::Deserialize;
44///
45/// #[derive(Deserialize)]
46/// struct Info {
47///     name: String,
48/// }
49///
50/// // extract `Info` from a path using serde
51/// #[get("/{name}")]
52/// async fn index(info: web::Path<Info>) -> String {
53///     format!("Welcome {}!", info.name)
54/// }
55/// ```
56///
57/// Segments matching multiple path components can be deserialized
58/// into a `Vec<_>` to percent-decode the components individually. Empty
59/// path components are ignored.
60///
61/// ```
62/// use actix_web::{get, web};
63/// use serde::Deserialize;
64///
65/// #[derive(Deserialize)]
66/// struct Tail {
67///     tail: Vec<String>,
68/// }
69///
70/// // extract `Tail` from a path using serde
71/// #[get("/path/to/{tail}*")]
72/// async fn index(info: web::Path<Tail>) -> String {
73///     format!("Navigating to {}!", info.tail.join(" :: "))
74/// }
75/// ```
76#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)]
77pub struct Path<T>(T);
78
79impl<T> Path<T> {
80    /// Unwrap into inner `T` value.
81    pub fn into_inner(self) -> T {
82        self.0
83    }
84}
85
86/// See [here](#Examples) for example of usage as an extractor.
87impl<T> FromRequest for Path<T>
88where
89    T: de::DeserializeOwned,
90{
91    type Error = Error;
92    type Future = Ready<Result<Self, Self::Error>>;
93
94    #[inline]
95    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
96        let error_handler = req
97            .app_data::<PathConfig>()
98            .or_else(|| req.app_data::<Data<PathConfig>>().map(Data::get_ref))
99            .and_then(|c| c.err_handler.clone());
100
101        ready(
102            de::Deserialize::deserialize(PathDeserializer::new(req.match_info()))
103                .map(Path)
104                .map_err(move |err| {
105                    log::debug!(
106                        "Failed during Path extractor deserialization. \
107                         Request path: {:?}",
108                        req.path()
109                    );
110
111                    if let Some(error_handler) = error_handler {
112                        let err = PathError::Deserialize(err);
113                        (error_handler)(err, req)
114                    } else {
115                        ErrorNotFound(err)
116                    }
117                }),
118        )
119    }
120}
121
122/// Path extractor configuration
123///
124/// ```
125/// use actix_web::web::PathConfig;
126/// use actix_web::{error, web, App, FromRequest, HttpResponse};
127/// use serde::Deserialize;
128///
129/// #[derive(Deserialize, Debug)]
130/// enum Folder {
131///     #[serde(rename = "inbox")]
132///     Inbox,
133///
134///     #[serde(rename = "outbox")]
135///     Outbox,
136/// }
137///
138/// // deserialize `Info` from request's path
139/// async fn index(folder: web::Path<Folder>) -> String {
140///     format!("Selected folder: {:?}!", folder)
141/// }
142///
143/// let app = App::new().service(
144///     web::resource("/messages/{folder}")
145///         .app_data(PathConfig::default().error_handler(|err, req| {
146///             error::InternalError::from_response(
147///                 err,
148///                 HttpResponse::Conflict().into(),
149///             )
150///             .into()
151///         }))
152///         .route(web::post().to(index)),
153/// );
154/// ```
155#[derive(Clone, Default)]
156pub struct PathConfig {
157    #[allow(clippy::type_complexity)]
158    err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>,
159}
160
161impl PathConfig {
162    /// Set custom error handler.
163    pub fn error_handler<F>(mut self, f: F) -> Self
164    where
165        F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static,
166    {
167        self.err_handler = Some(Arc::new(f));
168        self
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use actix_router::ResourceDef;
175    use derive_more::Display;
176    use serde::Deserialize;
177
178    use super::*;
179    use crate::{error, http, test::TestRequest, HttpResponse};
180
181    #[derive(Deserialize, Debug, Display)]
182    #[display("MyStruct({}, {})", key, value)]
183    struct MyStruct {
184        key: String,
185        value: String,
186    }
187
188    #[derive(Deserialize)]
189    struct Test2 {
190        key: String,
191        value: u32,
192    }
193
194    #[actix_rt::test]
195    async fn test_extract_path_single() {
196        let resource = ResourceDef::new("/{value}/");
197
198        let mut req = TestRequest::with_uri("/32/").to_srv_request();
199        resource.capture_match_info(req.match_info_mut());
200
201        let (req, mut pl) = req.into_parts();
202        assert_eq!(*Path::<i8>::from_request(&req, &mut pl).await.unwrap(), 32);
203        assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err());
204    }
205
206    #[allow(clippy::let_unit_value)]
207    #[actix_rt::test]
208    async fn test_tuple_extract() {
209        let resource = ResourceDef::new("/{key}/{value}/");
210
211        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
212        resource.capture_match_info(req.match_info_mut());
213
214        let (req, mut pl) = req.into_parts();
215        let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl)
216            .await
217            .unwrap();
218        assert_eq!(res.0, "name");
219        assert_eq!(res.1, "user1");
220
221        let (Path(a), Path(b)) =
222            <(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl)
223                .await
224                .unwrap();
225        assert_eq!(a.0, "name");
226        assert_eq!(a.1, "user1");
227        assert_eq!(b.0, "name");
228        assert_eq!(b.1, "user1");
229
230        let () = <()>::from_request(&req, &mut pl).await.unwrap();
231    }
232
233    #[actix_rt::test]
234    async fn test_request_extract() {
235        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
236
237        let resource = ResourceDef::new("/{key}/{value}/");
238        resource.capture_match_info(req.match_info_mut());
239
240        let (req, mut pl) = req.into_parts();
241        let mut s = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
242        assert_eq!(s.key, "name");
243        assert_eq!(s.value, "user1");
244        s.value = "user2".to_string();
245        assert_eq!(s.value, "user2");
246        assert_eq!(
247            format!("{}, {:?}", s, s),
248            "MyStruct(name, user2), Path(MyStruct { key: \"name\", value: \"user2\" })"
249        );
250        let s = s.into_inner();
251        assert_eq!(s.value, "user2");
252
253        let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl)
254            .await
255            .unwrap();
256        assert_eq!(s.0, "name");
257        assert_eq!(s.1, "user1");
258
259        let mut req = TestRequest::with_uri("/name/32/").to_srv_request();
260        let resource = ResourceDef::new("/{key}/{value}/");
261        resource.capture_match_info(req.match_info_mut());
262
263        let (req, mut pl) = req.into_parts();
264        let s = Path::<Test2>::from_request(&req, &mut pl).await.unwrap();
265        assert_eq!(s.as_ref().key, "name");
266        assert_eq!(s.value, 32);
267
268        let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl)
269            .await
270            .unwrap();
271        assert_eq!(s.0, "name");
272        assert_eq!(s.1, 32);
273
274        let res = Path::<Vec<String>>::from_request(&req, &mut pl)
275            .await
276            .unwrap();
277        assert_eq!(res[0], "name".to_owned());
278        assert_eq!(res[1], "32".to_owned());
279    }
280
281    #[actix_rt::test]
282    async fn paths_decoded() {
283        let resource = ResourceDef::new("/{key}/{value}");
284        let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%254%32").to_srv_request();
285        resource.capture_match_info(req.match_info_mut());
286
287        let (req, mut pl) = req.into_parts();
288        let path_items = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
289        assert_eq!(path_items.key, "na+me");
290        assert_eq!(path_items.value, "us/er%42");
291        assert_eq!(req.match_info().as_str(), "/na%2Bme/us%2Fer%2542");
292    }
293
294    #[actix_rt::test]
295    async fn test_custom_err_handler() {
296        let (req, mut pl) = TestRequest::with_uri("/name/user1/")
297            .app_data(PathConfig::default().error_handler(|err, _| {
298                error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into()
299            }))
300            .to_http_parts();
301
302        let s = Path::<(usize,)>::from_request(&req, &mut pl)
303            .await
304            .unwrap_err();
305        let res = HttpResponse::from_error(s);
306
307        assert_eq!(res.status(), http::StatusCode::CONFLICT);
308    }
309}