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#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)]
57pub struct Path<T>(T);
58
59impl<T> Path<T> {
60    /// Unwrap into inner `T` value.
61    pub fn into_inner(self) -> T {
62        self.0
63    }
64}
65
66/// See [here](#Examples) for example of usage as an extractor.
67impl<T> FromRequest for Path<T>
68where
69    T: de::DeserializeOwned,
70{
71    type Error = Error;
72    type Future = Ready<Result<Self, Self::Error>>;
73
74    #[inline]
75    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
76        let error_handler = req
77            .app_data::<PathConfig>()
78            .or_else(|| req.app_data::<Data<PathConfig>>().map(Data::get_ref))
79            .and_then(|c| c.err_handler.clone());
80
81        ready(
82            de::Deserialize::deserialize(PathDeserializer::new(req.match_info()))
83                .map(Path)
84                .map_err(move |err| {
85                    log::debug!(
86                        "Failed during Path extractor deserialization. \
87                         Request path: {:?}",
88                        req.path()
89                    );
90
91                    if let Some(error_handler) = error_handler {
92                        let err = PathError::Deserialize(err);
93                        (error_handler)(err, req)
94                    } else {
95                        ErrorNotFound(err)
96                    }
97                }),
98        )
99    }
100}
101
102/// Path extractor configuration
103///
104/// ```
105/// use actix_web::web::PathConfig;
106/// use actix_web::{error, web, App, FromRequest, HttpResponse};
107/// use serde::Deserialize;
108///
109/// #[derive(Deserialize, Debug)]
110/// enum Folder {
111///     #[serde(rename = "inbox")]
112///     Inbox,
113///
114///     #[serde(rename = "outbox")]
115///     Outbox,
116/// }
117///
118/// // deserialize `Info` from request's path
119/// async fn index(folder: web::Path<Folder>) -> String {
120///     format!("Selected folder: {:?}!", folder)
121/// }
122///
123/// let app = App::new().service(
124///     web::resource("/messages/{folder}")
125///         .app_data(PathConfig::default().error_handler(|err, req| {
126///             error::InternalError::from_response(
127///                 err,
128///                 HttpResponse::Conflict().into(),
129///             )
130///             .into()
131///         }))
132///         .route(web::post().to(index)),
133/// );
134/// ```
135#[derive(Clone, Default)]
136pub struct PathConfig {
137    #[allow(clippy::type_complexity)]
138    err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>,
139}
140
141impl PathConfig {
142    /// Set custom error handler.
143    pub fn error_handler<F>(mut self, f: F) -> Self
144    where
145        F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static,
146    {
147        self.err_handler = Some(Arc::new(f));
148        self
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use actix_router::ResourceDef;
155    use derive_more::Display;
156    use serde::Deserialize;
157
158    use super::*;
159    use crate::{error, http, test::TestRequest, HttpResponse};
160
161    #[derive(Deserialize, Debug, Display)]
162    #[display("MyStruct({}, {})", key, value)]
163    struct MyStruct {
164        key: String,
165        value: String,
166    }
167
168    #[derive(Deserialize)]
169    struct Test2 {
170        key: String,
171        value: u32,
172    }
173
174    #[actix_rt::test]
175    async fn test_extract_path_single() {
176        let resource = ResourceDef::new("/{value}/");
177
178        let mut req = TestRequest::with_uri("/32/").to_srv_request();
179        resource.capture_match_info(req.match_info_mut());
180
181        let (req, mut pl) = req.into_parts();
182        assert_eq!(*Path::<i8>::from_request(&req, &mut pl).await.unwrap(), 32);
183        assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err());
184    }
185
186    #[allow(clippy::let_unit_value)]
187    #[actix_rt::test]
188    async fn test_tuple_extract() {
189        let resource = ResourceDef::new("/{key}/{value}/");
190
191        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
192        resource.capture_match_info(req.match_info_mut());
193
194        let (req, mut pl) = req.into_parts();
195        let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl)
196            .await
197            .unwrap();
198        assert_eq!(res.0, "name");
199        assert_eq!(res.1, "user1");
200
201        let (Path(a), Path(b)) =
202            <(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl)
203                .await
204                .unwrap();
205        assert_eq!(a.0, "name");
206        assert_eq!(a.1, "user1");
207        assert_eq!(b.0, "name");
208        assert_eq!(b.1, "user1");
209
210        let () = <()>::from_request(&req, &mut pl).await.unwrap();
211    }
212
213    #[actix_rt::test]
214    async fn test_request_extract() {
215        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
216
217        let resource = ResourceDef::new("/{key}/{value}/");
218        resource.capture_match_info(req.match_info_mut());
219
220        let (req, mut pl) = req.into_parts();
221        let mut s = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
222        assert_eq!(s.key, "name");
223        assert_eq!(s.value, "user1");
224        s.value = "user2".to_string();
225        assert_eq!(s.value, "user2");
226        assert_eq!(
227            format!("{}, {:?}", s, s),
228            "MyStruct(name, user2), Path(MyStruct { key: \"name\", value: \"user2\" })"
229        );
230        let s = s.into_inner();
231        assert_eq!(s.value, "user2");
232
233        let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl)
234            .await
235            .unwrap();
236        assert_eq!(s.0, "name");
237        assert_eq!(s.1, "user1");
238
239        let mut req = TestRequest::with_uri("/name/32/").to_srv_request();
240        let resource = ResourceDef::new("/{key}/{value}/");
241        resource.capture_match_info(req.match_info_mut());
242
243        let (req, mut pl) = req.into_parts();
244        let s = Path::<Test2>::from_request(&req, &mut pl).await.unwrap();
245        assert_eq!(s.as_ref().key, "name");
246        assert_eq!(s.value, 32);
247
248        let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl)
249            .await
250            .unwrap();
251        assert_eq!(s.0, "name");
252        assert_eq!(s.1, 32);
253
254        let res = Path::<Vec<String>>::from_request(&req, &mut pl)
255            .await
256            .unwrap();
257        assert_eq!(res[0], "name".to_owned());
258        assert_eq!(res[1], "32".to_owned());
259    }
260
261    #[actix_rt::test]
262    async fn paths_decoded() {
263        let resource = ResourceDef::new("/{key}/{value}");
264        let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%254%32").to_srv_request();
265        resource.capture_match_info(req.match_info_mut());
266
267        let (req, mut pl) = req.into_parts();
268        let path_items = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
269        assert_eq!(path_items.key, "na+me");
270        assert_eq!(path_items.value, "us/er%42");
271        assert_eq!(req.match_info().as_str(), "/na%2Bme/us%2Fer%2542");
272    }
273
274    #[actix_rt::test]
275    async fn test_custom_err_handler() {
276        let (req, mut pl) = TestRequest::with_uri("/name/user1/")
277            .app_data(PathConfig::default().error_handler(|err, _| {
278                error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into()
279            }))
280            .to_http_parts();
281
282        let s = Path::<(usize,)>::from_request(&req, &mut pl)
283            .await
284            .unwrap_err();
285        let res = HttpResponse::from_error(s);
286
287        assert_eq!(res.status(), http::StatusCode::CONFLICT);
288    }
289}