Skip to main content

actix_web_lab/
url_encoded_form.rs

1//! URL-encoded form extractor with const-generic payload size limit.
2
3use std::{
4    fmt,
5    marker::PhantomData,
6    pin::Pin,
7    task::{Context, Poll, ready},
8};
9
10use actix_web::{
11    Error, FromRequest, HttpMessage, HttpRequest, ResponseError,
12    dev::Payload,
13    error::PayloadError,
14    http::{StatusCode, header},
15    web,
16};
17use derive_more::{Display, Error};
18use futures_util::Stream as _;
19use serde::de::DeserializeOwned;
20use tracing::debug;
21
22/// Default URL-encoded form payload size limit of 2MiB.
23pub const DEFAULT_URL_ENCODED_FORM_LIMIT: usize = 2_097_152;
24
25/// URL-encoded form extractor with const-generic payload size limit.
26///
27/// `UrlEncodedForm` is used to extract typed data from URL-encoded request payloads.
28///
29/// # Extractor
30/// To extract typed data from a request body, the inner type `T` must implement the
31/// [`serde::Deserialize`] trait.
32///
33/// Use the `LIMIT` const generic parameter to control the payload size limit. The default limit
34/// that is exported (`DEFAULT_LIMIT`) is 2MiB.
35///
36/// ```
37/// use actix_web::{App, post};
38/// use actix_web_lab::extract::{DEFAULT_URL_ENCODED_FORM_LIMIT, UrlEncodedForm};
39/// use serde::Deserialize;
40///
41/// #[derive(Deserialize)]
42/// struct Info {
43///     username: String,
44/// }
45///
46/// /// Deserialize `Info` from request's body.
47/// #[post("/")]
48/// async fn index(info: UrlEncodedForm<Info>) -> String {
49///     format!("Welcome {}!", info.username)
50/// }
51///
52/// const LIMIT_32_MB: usize = 33_554_432;
53///
54/// /// Deserialize payload with a higher 32MiB limit.
55/// #[post("/big-payload")]
56/// async fn big_payload(info: UrlEncodedForm<Info, LIMIT_32_MB>) -> String {
57///     format!("Welcome {}!", info.username)
58/// }
59/// ```
60#[doc(alias = "html_form", alias = "html form", alias = "form")]
61#[derive(Debug)]
62// #[derive(Debug, Deref, DerefMut, Display)]
63pub struct UrlEncodedForm<T, const LIMIT: usize = DEFAULT_URL_ENCODED_FORM_LIMIT>(pub T);
64
65mod waiting_on_derive_more_to_start_using_syn_2_due_to_proc_macro_panic {
66    use super::*;
67
68    impl<T, const LIMIT: usize> std::ops::Deref for UrlEncodedForm<T, LIMIT> {
69        type Target = T;
70
71        fn deref(&self) -> &Self::Target {
72            &self.0
73        }
74    }
75
76    impl<T, const LIMIT: usize> std::ops::DerefMut for UrlEncodedForm<T, LIMIT> {
77        fn deref_mut(&mut self) -> &mut Self::Target {
78            &mut self.0
79        }
80    }
81
82    impl<T: std::fmt::Display, const LIMIT: usize> std::fmt::Display for UrlEncodedForm<T, LIMIT> {
83        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84            std::fmt::Display::fmt(&self.0, f)
85        }
86    }
87}
88
89impl<T, const LIMIT: usize> UrlEncodedForm<T, LIMIT> {
90    /// Unwraps into inner `T` value.
91    pub fn into_inner(self) -> T {
92        self.0
93    }
94}
95
96/// See [here](#extractor) for example of usage as an extractor.
97impl<T: DeserializeOwned, const LIMIT: usize> FromRequest for UrlEncodedForm<T, LIMIT> {
98    type Error = Error;
99    type Future = UrlEncodedFormExtractFut<T, LIMIT>;
100
101    #[inline]
102    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
103        UrlEncodedFormExtractFut {
104            req: Some(req.clone()),
105            fut: UrlEncodedFormBody::new(req, payload),
106        }
107    }
108}
109
110#[allow(missing_debug_implementations)]
111pub struct UrlEncodedFormExtractFut<T, const LIMIT: usize> {
112    req: Option<HttpRequest>,
113    fut: UrlEncodedFormBody<T, LIMIT>,
114}
115
116impl<T: DeserializeOwned, const LIMIT: usize> Future for UrlEncodedFormExtractFut<T, LIMIT> {
117    type Output = Result<UrlEncodedForm<T, LIMIT>, Error>;
118
119    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
120        let this = self.get_mut();
121
122        let res = ready!(Pin::new(&mut this.fut).poll(cx));
123
124        let res = match res {
125            Err(err) => {
126                let req = this.req.take().unwrap();
127                debug!(
128                    "Failed to deserialize UrlEncodedForm<{}> from payload in handler: {}",
129                    core::any::type_name::<T>(),
130                    req.match_name().unwrap_or_else(|| req.path())
131                );
132
133                Err(err.into())
134            }
135            Ok(data) => Ok(UrlEncodedForm(data)),
136        };
137
138        Poll::Ready(res)
139    }
140}
141
142/// Future that resolves to some `T` when parsed from a URL-encoded payload.
143///
144/// Can deserialize any type `T` that implements [`Deserialize`][serde::Deserialize].
145///
146/// Returns error if:
147/// - `Content-Type` is not `application/x-www-form-urlencoded`.
148/// - `Content-Length` is greater than `LIMIT`.
149/// - The payload, when consumed, is not URL-encoded.
150pub enum UrlEncodedFormBody<T, const LIMIT: usize> {
151    Error(Option<UrlEncodedFormError>),
152    Body {
153        /// Length as reported by `Content-Length` header, if present.
154        #[allow(dead_code)]
155        length: Option<usize>,
156        payload: Payload,
157        buf: web::BytesMut,
158        _res: PhantomData<fn() -> T>,
159    },
160}
161
162impl<T: DeserializeOwned, const LIMIT: usize> UrlEncodedFormBody<T, LIMIT> {
163    /// Create a new future to decode a URL-encoded request payload.
164    pub fn new(req: &HttpRequest, payload: &mut Payload) -> Self {
165        // check content-type
166        let can_parse_form = if let Ok(Some(mime)) = req.mime_type() {
167            mime == mime::APPLICATION_WWW_FORM_URLENCODED
168        } else {
169            false
170        };
171
172        if !can_parse_form {
173            return UrlEncodedFormBody::Error(Some(UrlEncodedFormError::ContentType));
174        }
175
176        let length = req
177            .headers()
178            .get(&header::CONTENT_LENGTH)
179            .and_then(|l| l.to_str().ok())
180            .and_then(|s| s.parse::<usize>().ok());
181
182        // Notice the content-length is not checked against config limit here.
183        // As the internal usage always call UrlEncodedBody::limit after UrlEncodedBody::new.
184        // And limit check to return an error variant of UrlEncodedBody happens there.
185
186        let payload = payload.take();
187
188        if let Some(len) = length
189            && len > LIMIT
190        {
191            return UrlEncodedFormBody::Error(Some(UrlEncodedFormError::Overflow {
192                size: len,
193                limit: LIMIT,
194            }));
195        }
196
197        UrlEncodedFormBody::Body {
198            length,
199            payload,
200            buf: web::BytesMut::with_capacity(8192),
201            _res: PhantomData,
202        }
203    }
204}
205
206impl<T: DeserializeOwned, const LIMIT: usize> Future for UrlEncodedFormBody<T, LIMIT> {
207    type Output = Result<T, UrlEncodedFormError>;
208
209    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
210        let this = self.get_mut();
211
212        match this {
213            UrlEncodedFormBody::Body { buf, payload, .. } => loop {
214                let res = ready!(Pin::new(&mut *payload).poll_next(cx));
215
216                match res {
217                    Some(chunk) => {
218                        let chunk =
219                            chunk.map_err(|err| UrlEncodedFormError::Payload { source: err })?;
220
221                        let buf_len = buf.len() + chunk.len();
222                        if buf_len > LIMIT {
223                            return Poll::Ready(Err(UrlEncodedFormError::Overflow {
224                                size: buf_len,
225                                limit: LIMIT,
226                            }));
227                        } else {
228                            buf.extend_from_slice(&chunk);
229                        }
230                    }
231
232                    None => {
233                        let de = serde_html_form::Deserializer::from_bytes(buf);
234
235                        let form = serde_path_to_error::deserialize(de).map_err(|err| {
236                            UrlEncodedFormError::Deserialize {
237                                source: UrlEncodedFormDeserializeError {
238                                    path: err.path().clone(),
239                                    source: err.into_inner(),
240                                },
241                            }
242                        })?;
243
244                        return Poll::Ready(Ok(form));
245                    }
246                }
247            },
248
249            UrlEncodedFormBody::Error(err) => Poll::Ready(Err(err.take().unwrap())),
250        }
251    }
252}
253
254/// Errors that can occur while extracting URL-encoded forms.
255#[derive(Debug, Display, Error)]
256#[non_exhaustive]
257pub enum UrlEncodedFormError {
258    /// Payload size is larger than allowed.
259    #[display(
260        "URL encoded payload is larger ({} bytes) than allowed (limit: {} bytes).",
261        size,
262        limit
263    )]
264    Overflow { size: usize, limit: usize },
265
266    /// Content type error.
267    #[display("Content type error.")]
268    ContentType,
269
270    /// Deserialization error.
271    #[display("Deserialization error")]
272    Deserialize {
273        /// Deserialization error.
274        source: UrlEncodedFormDeserializeError,
275    },
276
277    /// Payload error.
278    #[display("Error that occur during reading payload")]
279    Payload { source: PayloadError },
280}
281
282impl ResponseError for UrlEncodedFormError {
283    fn status_code(&self) -> StatusCode {
284        match self {
285            Self::Overflow { .. } => StatusCode::PAYLOAD_TOO_LARGE,
286            Self::ContentType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
287            Self::Payload { source: err } => err.status_code(),
288            Self::Deserialize { .. } => StatusCode::UNPROCESSABLE_ENTITY,
289        }
290    }
291}
292
293/// Errors that can occur while deserializing URL-encoded forms query strings.
294#[derive(Debug, Error)]
295pub struct UrlEncodedFormDeserializeError {
296    /// Path where deserialization error occurred.
297    path: serde_path_to_error::Path,
298
299    /// Deserialization error.
300    source: serde_html_form::de::Error,
301}
302
303impl UrlEncodedFormDeserializeError {
304    /// Returns the path at which the deserialization error occurred.
305    pub fn path(&self) -> impl fmt::Display + '_ {
306        &self.path
307    }
308
309    /// Returns the source error.
310    pub fn source(&self) -> &serde_html_form::de::Error {
311        &self.source
312    }
313}
314
315impl fmt::Display for UrlEncodedFormDeserializeError {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        f.write_str("URL-encoded form deserialization failed")?;
318
319        if self.path.iter().len() > 0 {
320            write!(f, " at path: {}", self.path)?;
321        }
322
323        Ok(())
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use actix_web::{http::header, test::TestRequest, web::Bytes};
330    use serde::{Deserialize, Serialize};
331
332    use super::*;
333
334    #[derive(Serialize, Deserialize, PartialEq, Debug)]
335    struct MyObject {
336        name: String,
337    }
338
339    fn err_eq(err: UrlEncodedFormError, other: UrlEncodedFormError) -> bool {
340        match err {
341            UrlEncodedFormError::Overflow { .. } => {
342                matches!(other, UrlEncodedFormError::Overflow { .. })
343            }
344
345            UrlEncodedFormError::ContentType => matches!(other, UrlEncodedFormError::ContentType),
346
347            _ => false,
348        }
349    }
350
351    #[actix_web::test]
352    async fn test_extract() {
353        let (req, mut pl) = TestRequest::default()
354            .insert_header(header::ContentType::form_url_encoded())
355            .insert_header((
356                header::CONTENT_LENGTH,
357                header::HeaderValue::from_static("9"),
358            ))
359            .set_payload(Bytes::from_static(b"name=test"))
360            .to_http_parts();
361
362        let s =
363            UrlEncodedForm::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::from_request(&req, &mut pl)
364                .await
365                .unwrap();
366        assert_eq!(s.name, "test");
367        assert_eq!(
368            s.into_inner(),
369            MyObject {
370                name: "test".to_string()
371            }
372        );
373
374        let (req, mut pl) = TestRequest::default()
375            .insert_header(header::ContentType::form_url_encoded())
376            .insert_header((
377                header::CONTENT_LENGTH,
378                header::HeaderValue::from_static("9"),
379            ))
380            .set_payload(Bytes::from_static(b"name=test"))
381            .to_http_parts();
382
383        let s = UrlEncodedForm::<MyObject, 8>::from_request(&req, &mut pl).await;
384        let err = format!("{}", s.unwrap_err());
385        assert_eq!(
386            err,
387            "URL encoded payload is larger (9 bytes) than allowed (limit: 8 bytes).",
388        );
389
390        let (req, mut pl) = TestRequest::default()
391            .insert_header(header::ContentType::form_url_encoded())
392            .insert_header((
393                header::CONTENT_LENGTH,
394                header::HeaderValue::from_static("9"),
395            ))
396            .set_payload(Bytes::from_static(b"name=test"))
397            .to_http_parts();
398        let s = UrlEncodedForm::<MyObject, 8>::from_request(&req, &mut pl).await;
399        let err = format!("{}", s.unwrap_err());
400        assert!(
401            err.contains("payload is larger") && err.contains("than allowed"),
402            "unexpected error string: {err:?}"
403        );
404    }
405
406    #[actix_web::test]
407    async fn test_form_body() {
408        let (req, mut pl) = TestRequest::default().to_http_parts();
409        let form =
410            UrlEncodedFormBody::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::new(&req, &mut pl)
411                .await;
412        assert!(err_eq(form.unwrap_err(), UrlEncodedFormError::ContentType));
413
414        let (req, mut pl) = TestRequest::default()
415            .insert_header((
416                header::CONTENT_TYPE,
417                header::HeaderValue::from_static("application/text"),
418            ))
419            .to_http_parts();
420        let form =
421            UrlEncodedFormBody::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::new(&req, &mut pl)
422                .await;
423        assert!(err_eq(form.unwrap_err(), UrlEncodedFormError::ContentType));
424
425        let (req, mut pl) = TestRequest::default()
426            .insert_header(header::ContentType::form_url_encoded())
427            .insert_header((
428                header::CONTENT_LENGTH,
429                header::HeaderValue::from_static("10000"),
430            ))
431            .to_http_parts();
432
433        let form = UrlEncodedFormBody::<MyObject, 100>::new(&req, &mut pl).await;
434        assert!(err_eq(
435            form.unwrap_err(),
436            UrlEncodedFormError::Overflow {
437                size: 10000,
438                limit: 100
439            }
440        ));
441
442        let (req, mut pl) = TestRequest::default()
443            .insert_header(header::ContentType::form_url_encoded())
444            .set_payload(Bytes::from_static(&[0u8; 1000]))
445            .to_http_parts();
446
447        let form = UrlEncodedFormBody::<MyObject, 100>::new(&req, &mut pl).await;
448
449        assert!(err_eq(
450            form.unwrap_err(),
451            UrlEncodedFormError::Overflow {
452                size: 1000,
453                limit: 100
454            }
455        ));
456
457        let (req, mut pl) = TestRequest::default()
458            .insert_header(header::ContentType::form_url_encoded())
459            .insert_header((
460                header::CONTENT_LENGTH,
461                header::HeaderValue::from_static("9"),
462            ))
463            .set_payload(Bytes::from_static(b"name=test"))
464            .to_http_parts();
465
466        let form =
467            UrlEncodedFormBody::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::new(&req, &mut pl)
468                .await;
469        assert_eq!(
470            form.ok().unwrap(),
471            MyObject {
472                name: "test".to_owned()
473            }
474        );
475    }
476
477    #[actix_web::test]
478    async fn test_with_form_and_bad_content_type() {
479        let (req, mut pl) = TestRequest::default()
480            .insert_header((
481                header::CONTENT_TYPE,
482                header::HeaderValue::from_static("text/plain"),
483            ))
484            .insert_header((
485                header::CONTENT_LENGTH,
486                header::HeaderValue::from_static("9"),
487            ))
488            .set_payload(Bytes::from_static(b"name=test"))
489            .to_http_parts();
490
491        let s = UrlEncodedForm::<MyObject, 4096>::from_request(&req, &mut pl).await;
492        assert!(s.is_err())
493    }
494
495    #[actix_web::test]
496    async fn test_with_config_in_data_wrapper() {
497        let (req, mut pl) = TestRequest::default()
498            .insert_header(header::ContentType::form_url_encoded())
499            .insert_header((header::CONTENT_LENGTH, 9))
500            .set_payload(Bytes::from_static(b"name=test"))
501            .to_http_parts();
502
503        let s = UrlEncodedForm::<MyObject, 8>::from_request(&req, &mut pl).await;
504        assert!(s.is_err());
505
506        let err_str = s.unwrap_err().to_string();
507        assert_eq!(
508            err_str,
509            "URL encoded payload is larger (9 bytes) than allowed (limit: 8 bytes).",
510        );
511    }
512}