axum_extra/extract/
form.rs1use 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#[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 pub struct FailedToDeserializeForm(Error);
79}
80
81define_rejection! {
82 #[status = UNPROCESSABLE_ENTITY]
83 #[body = "Failed to deserialize form body"]
84 pub struct FailedToDeserializeFormBody(Error);
87}
88
89composite_rejection! {
90 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}