rama_http/service/web/endpoint/extract/body/
form.rs1use 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 pub struct InvalidFormContentType;
18}
19
20define_http_rejection! {
21 #[status = BAD_REQUEST]
22 #[body = "Failed to deserialize form"]
23 pub struct FailedToDeserializeForm(Error);
26}
27
28composite_http_rejection! {
29 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
88fn 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}