Skip to main content

rama_http/service/web/endpoint/extract/body/
form.rs

1use rama_core::bytes::Bytes;
2
3use super::BytesRejection;
4use crate::body::util::BodyExt;
5use crate::service::web::extract::{FromRequest, FromRequestBody};
6use crate::utils::macros::{composite_http_rejection, define_http_rejection};
7use crate::{Method, Request};
8
9pub use crate::service::web::endpoint::response::Form;
10
11define_http_rejection! {
12    #[status = UNSUPPORTED_MEDIA_TYPE]
13    #[body = "Form requests must have `Content-Type: application/x-www-form-urlencoded`"]
14    /// Rejection type for [`Form`]
15    /// used if the `Content-Type` header is missing
16    /// or its value is not `application/x-www-form-urlencoded`.
17    pub struct InvalidFormContentType;
18}
19
20define_http_rejection! {
21    #[status = BAD_REQUEST]
22    #[body = "Failed to deserialize form"]
23    /// Rejection type used if the [`Form`]
24    /// deserialize the form into the target type.
25    pub struct FailedToDeserializeForm(Error);
26}
27
28composite_http_rejection! {
29    /// Rejection used for [`Form`]
30    ///
31    /// Contains one variant for each way the [`Form`] extractor
32    /// can fail.
33    pub enum FormRejection {
34        InvalidFormContentType,
35        FailedToDeserializeForm,
36        BytesRejection,
37    }
38}
39
40impl<T> FromRequest for Form<T>
41where
42    T: serde::de::DeserializeOwned + Send + Sync + 'static,
43{
44    type Rejection = FormRejection;
45
46    async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
47        let (parts, body) = req.into_parts();
48        let future = Self::from_request_body(&parts, body);
49        future.await
50    }
51}
52
53impl<T> FromRequestBody for Form<T>
54where
55    T: serde::de::DeserializeOwned + Send + Sync + 'static,
56{
57    fn from_request_body(
58        parts: &crate::request::Parts,
59        body: crate::Body,
60    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send + 'static {
61        let query_result: Option<Result<Self, FormRejection>> = if parts.method == Method::GET {
62            Some(
63                parts
64                    .uri
65                    .query_params()
66                    .map(Self)
67                    .map_err(|err| FailedToDeserializeForm::from_err(err).into()),
68            )
69        } else {
70            None
71        };
72        let body_bytes = extract_form_body_bytes(parts, body);
73
74        async move {
75            if let Some(result) = query_result {
76                return result;
77            }
78
79            let bytes = body_bytes.await?;
80            Ok(Self(match serde_html_form::from_bytes(&bytes) {
81                Ok(value) => value,
82                Err(err) => return Err(FailedToDeserializeForm::from_err(err).into()),
83            }))
84        }
85    }
86}
87
88// Kept non-generic so body collection is compiled once rather than once per form type.
89fn extract_form_body_bytes(
90    parts: &crate::request::Parts,
91    body: crate::Body,
92) -> impl Future<Output = Result<Bytes, FormRejection>> + Send + 'static {
93    let has_valid_content_type = crate::service::web::extract::has_any_content_type(
94        &parts.headers,
95        &[&crate::mime::APPLICATION_WWW_FORM_URLENCODED],
96    );
97
98    async move {
99        if !has_valid_content_type {
100            return Err(InvalidFormContentType.into());
101        }
102
103        body.collect()
104            .await
105            .map_err(BytesRejection::from_err)
106            .map(|body| body.to_bytes())
107            .map_err(Into::into)
108    }
109}
110
111#[cfg(test)]
112mod test {
113    use super::*;
114    use crate::service::web::WebService;
115    use crate::{Body, Method, Request, StatusCode};
116    use rama_core::Service;
117
118    #[tokio::test]
119    async fn test_form_post_form_urlencoded() {
120        #[derive(Debug, serde::Deserialize)]
121        struct Input {
122            name: String,
123            age: u8,
124        }
125
126        let service = WebService::default().with_post("/", async |Form(body): Form<Input>| {
127            assert_eq!(body.name, "Devan");
128            assert_eq!(body.age, 29);
129        });
130
131        let req = Request::builder()
132            .uri("/")
133            .method(Method::POST)
134            .header("content-type", "application/x-www-form-urlencoded")
135            .body(r#"name=Devan&age=29"#.into())
136            .unwrap();
137        let resp = service.serve(req).await.unwrap();
138        assert_eq!(resp.status(), StatusCode::OK);
139    }
140
141    #[tokio::test]
142    async fn test_form_post_form_urlencoded_missing_data_fail() {
143        #[derive(Debug, serde::Deserialize)]
144        #[expect(dead_code)]
145        struct Input {
146            name: String,
147            age: u8,
148        }
149
150        let service =
151            WebService::default().with_post("/", async |Form(_): Form<Input>| StatusCode::OK);
152
153        let req = Request::builder()
154            .uri("/")
155            .method(Method::POST)
156            .header("content-type", "application/x-www-form-urlencoded")
157            .body(r#"age=29"#.into())
158            .unwrap();
159        let resp = service.serve(req).await.unwrap();
160        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
161    }
162
163    #[tokio::test]
164    async fn test_form_get_form_urlencoded_fail() {
165        #[derive(Debug, serde::Deserialize)]
166        #[expect(dead_code)]
167        struct Input {
168            name: String,
169            age: u8,
170        }
171
172        let service =
173            WebService::default().with_get("/", async |Form(_): Form<Input>| StatusCode::OK);
174
175        let req = Request::builder()
176            .uri("/")
177            .method(Method::GET)
178            .header("content-type", "application/x-www-form-urlencoded")
179            .body(r#"name=Devan&age=29"#.into())
180            .unwrap();
181        let resp = service.serve(req).await.unwrap();
182        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
183    }
184
185    #[tokio::test]
186    async fn test_form_get() {
187        #[derive(Debug, serde::Deserialize)]
188        struct Input {
189            name: String,
190            age: u8,
191        }
192
193        let service = WebService::default().with_get("/", async |Form(body): Form<Input>| {
194            assert_eq!(body.name, "Devan");
195            assert_eq!(body.age, 29);
196        });
197
198        let req = Request::builder()
199            .uri("/?name=Devan&age=29")
200            .method(Method::GET)
201            .body(Body::empty())
202            .unwrap();
203        let resp = service.serve(req).await.unwrap();
204        assert_eq!(resp.status(), StatusCode::OK);
205    }
206
207    #[tokio::test]
208    async fn test_form_get_fail_missing_data() {
209        #[derive(Debug, serde::Deserialize)]
210        #[expect(dead_code)]
211        struct Input {
212            name: String,
213            age: u8,
214        }
215
216        let service =
217            WebService::default().with_get("/", async |Form(_): Form<Input>| StatusCode::OK);
218
219        let req = Request::builder()
220            .uri("/?name=Devan")
221            .method(Method::GET)
222            .body(Body::empty())
223            .unwrap();
224        let resp = service.serve(req).await.unwrap();
225        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
226    }
227}