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_core::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<T>,
159    },
160}
161
162impl<T, const LIMIT: usize> Unpin for UrlEncodedFormBody<T, LIMIT> {}
163
164impl<T: DeserializeOwned, const LIMIT: usize> UrlEncodedFormBody<T, LIMIT> {
165    /// Create a new future to decode a URL-encoded request payload.
166    pub fn new(req: &HttpRequest, payload: &mut Payload) -> Self {
167        // check content-type
168        let can_parse_form = if let Ok(Some(mime)) = req.mime_type() {
169            mime == mime::APPLICATION_WWW_FORM_URLENCODED
170        } else {
171            false
172        };
173
174        if !can_parse_form {
175            return UrlEncodedFormBody::Error(Some(UrlEncodedFormError::ContentType));
176        }
177
178        let length = req
179            .headers()
180            .get(&header::CONTENT_LENGTH)
181            .and_then(|l| l.to_str().ok())
182            .and_then(|s| s.parse::<usize>().ok());
183
184        // Notice the content-length is not checked against config limit here.
185        // As the internal usage always call UrlEncodedBody::limit after UrlEncodedBody::new.
186        // And limit check to return an error variant of UrlEncodedBody happens there.
187
188        let payload = payload.take();
189
190        if let Some(len) = length {
191            if len > LIMIT {
192                return UrlEncodedFormBody::Error(Some(UrlEncodedFormError::Overflow {
193                    size: len,
194                    limit: LIMIT,
195                }));
196            }
197        }
198
199        UrlEncodedFormBody::Body {
200            length,
201            payload,
202            buf: web::BytesMut::with_capacity(8192),
203            _res: PhantomData,
204        }
205    }
206}
207
208impl<T: DeserializeOwned, const LIMIT: usize> Future for UrlEncodedFormBody<T, LIMIT> {
209    type Output = Result<T, UrlEncodedFormError>;
210
211    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
212        let this = self.get_mut();
213
214        match this {
215            UrlEncodedFormBody::Body { buf, payload, .. } => loop {
216                let res = ready!(Pin::new(&mut *payload).poll_next(cx));
217
218                match res {
219                    Some(chunk) => {
220                        let chunk =
221                            chunk.map_err(|err| UrlEncodedFormError::Payload { source: err })?;
222
223                        let buf_len = buf.len() + chunk.len();
224                        if buf_len > LIMIT {
225                            return Poll::Ready(Err(UrlEncodedFormError::Overflow {
226                                size: buf_len,
227                                limit: LIMIT,
228                            }));
229                        } else {
230                            buf.extend_from_slice(&chunk);
231                        }
232                    }
233
234                    None => {
235                        let de = serde_html_form::Deserializer::from_bytes(buf);
236
237                        let form = serde_path_to_error::deserialize(de).map_err(|err| {
238                            UrlEncodedFormError::Deserialize {
239                                source: UrlEncodedFormDeserializeError {
240                                    path: err.path().clone(),
241                                    source: err.into_inner(),
242                                },
243                            }
244                        })?;
245
246                        return Poll::Ready(Ok(form));
247                    }
248                }
249            },
250
251            UrlEncodedFormBody::Error(err) => Poll::Ready(Err(err.take().unwrap())),
252        }
253    }
254}
255
256/// Errors that can occur while extracting URL-encoded forms.
257#[derive(Debug, Display, Error)]
258#[non_exhaustive]
259pub enum UrlEncodedFormError {
260    /// Payload size is larger than allowed.
261    #[display(
262        "URL encoded payload is larger ({} bytes) than allowed (limit: {} bytes).",
263        size,
264        limit
265    )]
266    Overflow { size: usize, limit: usize },
267
268    /// Content type error.
269    #[display("Content type error.")]
270    ContentType,
271
272    /// Deserialization error.
273    #[display("Deserialization error")]
274    Deserialize {
275        /// Deserialization error.
276        source: UrlEncodedFormDeserializeError,
277    },
278
279    /// Payload error.
280    #[display("Error that occur during reading payload")]
281    Payload { source: PayloadError },
282}
283
284impl ResponseError for UrlEncodedFormError {
285    fn status_code(&self) -> StatusCode {
286        match self {
287            Self::Overflow { .. } => StatusCode::PAYLOAD_TOO_LARGE,
288            Self::ContentType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
289            Self::Payload { source: err } => err.status_code(),
290            Self::Deserialize { .. } => StatusCode::UNPROCESSABLE_ENTITY,
291        }
292    }
293}
294
295/// Errors that can occur while deserializing URL-encoded forms query strings.
296#[derive(Debug, Error)]
297pub struct UrlEncodedFormDeserializeError {
298    /// Path where deserialization error occurred.
299    path: serde_path_to_error::Path,
300
301    /// Deserialization error.
302    source: serde_html_form::de::Error,
303}
304
305impl UrlEncodedFormDeserializeError {
306    /// Returns the path at which the deserialization error occurred.
307    pub fn path(&self) -> impl fmt::Display + '_ {
308        &self.path
309    }
310
311    /// Returns the source error.
312    pub fn source(&self) -> &serde_html_form::de::Error {
313        &self.source
314    }
315}
316
317impl fmt::Display for UrlEncodedFormDeserializeError {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        f.write_str("URL-encoded form deserialization failed")?;
320
321        if self.path.iter().len() > 0 {
322            write!(f, " at path: {}", &self.path)?;
323        }
324
325        Ok(())
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use actix_web::{http::header, test::TestRequest, web::Bytes};
332    use serde::{Deserialize, Serialize};
333
334    use super::*;
335
336    #[derive(Serialize, Deserialize, PartialEq, Debug)]
337    struct MyObject {
338        name: String,
339    }
340
341    fn err_eq(err: UrlEncodedFormError, other: UrlEncodedFormError) -> bool {
342        match err {
343            UrlEncodedFormError::Overflow { .. } => {
344                matches!(other, UrlEncodedFormError::Overflow { .. })
345            }
346
347            UrlEncodedFormError::ContentType => matches!(other, UrlEncodedFormError::ContentType),
348
349            _ => false,
350        }
351    }
352
353    #[actix_web::test]
354    async fn test_extract() {
355        let (req, mut pl) = TestRequest::default()
356            .insert_header(header::ContentType::form_url_encoded())
357            .insert_header((
358                header::CONTENT_LENGTH,
359                header::HeaderValue::from_static("9"),
360            ))
361            .set_payload(Bytes::from_static(b"name=test"))
362            .to_http_parts();
363
364        let s =
365            UrlEncodedForm::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::from_request(&req, &mut pl)
366                .await
367                .unwrap();
368        assert_eq!(s.name, "test");
369        assert_eq!(
370            s.into_inner(),
371            MyObject {
372                name: "test".to_string()
373            }
374        );
375
376        let (req, mut pl) = TestRequest::default()
377            .insert_header(header::ContentType::form_url_encoded())
378            .insert_header((
379                header::CONTENT_LENGTH,
380                header::HeaderValue::from_static("9"),
381            ))
382            .set_payload(Bytes::from_static(b"name=test"))
383            .to_http_parts();
384
385        let s = UrlEncodedForm::<MyObject, 8>::from_request(&req, &mut pl).await;
386        let err = format!("{}", s.unwrap_err());
387        assert_eq!(
388            err,
389            "URL encoded payload is larger (9 bytes) than allowed (limit: 8 bytes).",
390        );
391
392        let (req, mut pl) = TestRequest::default()
393            .insert_header(header::ContentType::form_url_encoded())
394            .insert_header((
395                header::CONTENT_LENGTH,
396                header::HeaderValue::from_static("9"),
397            ))
398            .set_payload(Bytes::from_static(b"name=test"))
399            .to_http_parts();
400        let s = UrlEncodedForm::<MyObject, 8>::from_request(&req, &mut pl).await;
401        let err = format!("{}", s.unwrap_err());
402        assert!(
403            err.contains("payload is larger") && err.contains("than allowed"),
404            "unexpected error string: {err:?}"
405        );
406    }
407
408    #[actix_web::test]
409    async fn test_form_body() {
410        let (req, mut pl) = TestRequest::default().to_http_parts();
411        let form =
412            UrlEncodedFormBody::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::new(&req, &mut pl)
413                .await;
414        assert!(err_eq(form.unwrap_err(), UrlEncodedFormError::ContentType));
415
416        let (req, mut pl) = TestRequest::default()
417            .insert_header((
418                header::CONTENT_TYPE,
419                header::HeaderValue::from_static("application/text"),
420            ))
421            .to_http_parts();
422        let form =
423            UrlEncodedFormBody::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::new(&req, &mut pl)
424                .await;
425        assert!(err_eq(form.unwrap_err(), UrlEncodedFormError::ContentType));
426
427        let (req, mut pl) = TestRequest::default()
428            .insert_header(header::ContentType::form_url_encoded())
429            .insert_header((
430                header::CONTENT_LENGTH,
431                header::HeaderValue::from_static("10000"),
432            ))
433            .to_http_parts();
434
435        let form = UrlEncodedFormBody::<MyObject, 100>::new(&req, &mut pl).await;
436        assert!(err_eq(
437            form.unwrap_err(),
438            UrlEncodedFormError::Overflow {
439                size: 10000,
440                limit: 100
441            }
442        ));
443
444        let (req, mut pl) = TestRequest::default()
445            .insert_header(header::ContentType::form_url_encoded())
446            .set_payload(Bytes::from_static(&[0u8; 1000]))
447            .to_http_parts();
448
449        let form = UrlEncodedFormBody::<MyObject, 100>::new(&req, &mut pl).await;
450
451        assert!(err_eq(
452            form.unwrap_err(),
453            UrlEncodedFormError::Overflow {
454                size: 1000,
455                limit: 100
456            }
457        ));
458
459        let (req, mut pl) = TestRequest::default()
460            .insert_header(header::ContentType::form_url_encoded())
461            .insert_header((
462                header::CONTENT_LENGTH,
463                header::HeaderValue::from_static("9"),
464            ))
465            .set_payload(Bytes::from_static(b"name=test"))
466            .to_http_parts();
467
468        let form =
469            UrlEncodedFormBody::<MyObject, DEFAULT_URL_ENCODED_FORM_LIMIT>::new(&req, &mut pl)
470                .await;
471        assert_eq!(
472            form.ok().unwrap(),
473            MyObject {
474                name: "test".to_owned()
475            }
476        );
477    }
478
479    #[actix_web::test]
480    async fn test_with_form_and_bad_content_type() {
481        let (req, mut pl) = TestRequest::default()
482            .insert_header((
483                header::CONTENT_TYPE,
484                header::HeaderValue::from_static("text/plain"),
485            ))
486            .insert_header((
487                header::CONTENT_LENGTH,
488                header::HeaderValue::from_static("9"),
489            ))
490            .set_payload(Bytes::from_static(b"name=test"))
491            .to_http_parts();
492
493        let s = UrlEncodedForm::<MyObject, 4096>::from_request(&req, &mut pl).await;
494        assert!(s.is_err())
495    }
496
497    #[actix_web::test]
498    async fn test_with_config_in_data_wrapper() {
499        let (req, mut pl) = TestRequest::default()
500            .insert_header(header::ContentType::form_url_encoded())
501            .insert_header((header::CONTENT_LENGTH, 9))
502            .set_payload(Bytes::from_static(b"name=test"))
503            .to_http_parts();
504
505        let s = UrlEncodedForm::<MyObject, 8>::from_request(&req, &mut pl).await;
506        assert!(s.is_err());
507
508        let err_str = s.unwrap_err().to_string();
509        assert_eq!(
510            err_str,
511            "URL encoded payload is larger (9 bytes) than allowed (limit: 8 bytes).",
512        );
513    }
514}