axum_extra/extract/
form.rs

1use axum::extract::rejection::RawFormRejection;
2use axum::{
3    extract::{FromRequest, RawForm, Request},
4    RequestExt,
5};
6use axum_core::__composite_rejection as composite_rejection;
7use axum_core::__define_rejection as define_rejection;
8use serde::de::DeserializeOwned;
9
10/// Extractor that deserializes `application/x-www-form-urlencoded` requests
11/// into some type.
12///
13/// `T` is expected to implement [`serde::Deserialize`].
14///
15/// # Differences from `axum::extract::Form`
16///
17/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
18/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
19/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
20/// container.
21///
22/// # Example
23///
24/// ```rust,no_run
25/// use axum_extra::extract::Form;
26/// use serde::Deserialize;
27///
28/// #[derive(Deserialize)]
29/// struct Payload {
30///     #[serde(rename = "value")]
31///     values: Vec<String>,
32/// }
33///
34/// async fn accept_form(Form(payload): Form<Payload>) {
35///     // ...
36/// }
37/// ```
38///
39/// [`serde_html_form`]: https://crates.io/crates/serde_html_form
40#[derive(Debug, Clone, Copy, Default)]
41#[cfg(feature = "form")]
42pub struct Form<T>(pub T);
43
44axum_core::__impl_deref!(Form);
45
46impl<T, S> FromRequest<S> for Form<T>
47where
48    T: DeserializeOwned,
49    S: Send + Sync,
50{
51    type Rejection = FormRejection;
52
53    async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
54        let is_get_or_head =
55            req.method() == http::Method::GET || req.method() == http::Method::HEAD;
56
57        let RawForm(bytes) = req.extract().await?;
58
59        let deserializer = serde_html_form::Deserializer::new(form_urlencoded::parse(&bytes));
60
61        serde_path_to_error::deserialize::<_, T>(deserializer)
62            .map(Self)
63            .map_err(|err| {
64                if is_get_or_head {
65                    FailedToDeserializeForm::from_err(err).into()
66                } else {
67                    FailedToDeserializeFormBody::from_err(err).into()
68                }
69            })
70    }
71}
72
73define_rejection! {
74    #[status = BAD_REQUEST]
75    #[body = "Failed to deserialize form"]
76    /// Rejection type used if the [`Form`](Form) extractor is unable to
77    /// deserialize the form into the target type.
78    pub struct FailedToDeserializeForm(Error);
79}
80
81define_rejection! {
82    #[status = UNPROCESSABLE_ENTITY]
83    #[body = "Failed to deserialize form body"]
84    /// Rejection type used if the [`Form`](Form) extractor is unable to
85    /// deserialize the form body into the target type.
86    pub struct FailedToDeserializeFormBody(Error);
87}
88
89composite_rejection! {
90    /// Rejection used for [`Form`].
91    ///
92    /// Contains one variant for each way the [`Form`] extractor can fail.
93    pub enum FormRejection {
94        RawFormRejection,
95        FailedToDeserializeForm,
96        FailedToDeserializeFormBody,
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::test_helpers::*;
104    use axum::routing::{on, post, MethodFilter};
105    use axum::Router;
106    use http::header::CONTENT_TYPE;
107    use http::StatusCode;
108    use mime::APPLICATION_WWW_FORM_URLENCODED;
109    use serde::Deserialize;
110
111    #[tokio::test]
112    async fn supports_multiple_values() {
113        #[derive(Deserialize)]
114        struct Data {
115            #[serde(rename = "value")]
116            values: Vec<String>,
117        }
118
119        let app = Router::new().route(
120            "/",
121            post(|Form(data): Form<Data>| async move { data.values.join(",") }),
122        );
123
124        let client = TestClient::new(app);
125
126        let res = client
127            .post("/")
128            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
129            .body("value=one&value=two")
130            .await;
131
132        assert_eq!(res.status(), StatusCode::OK);
133        assert_eq!(res.text().await, "one,two");
134    }
135
136    #[tokio::test]
137    async fn deserialize_error_status_codes() {
138        #[allow(dead_code)]
139        #[derive(Deserialize)]
140        struct Payload {
141            a: i32,
142        }
143
144        let app = Router::new().route(
145            "/",
146            on(
147                MethodFilter::GET.or(MethodFilter::POST),
148                |_: Form<Payload>| async {},
149            ),
150        );
151
152        let client = TestClient::new(app);
153
154        let res = client.get("/?a=false").await;
155        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
156        assert_eq!(
157            res.text().await,
158            "Failed to deserialize form: a: invalid digit found in string"
159        );
160
161        let res = client
162            .post("/")
163            .header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
164            .body("a=false")
165            .await;
166        assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
167        assert_eq!(
168            res.text().await,
169            "Failed to deserialize form body: a: invalid digit found in string"
170        );
171    }
172}