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