actix_web_lab/
url_encoded_form.rs

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