axum_extra/extract/
query.rs1use axum::extract::FromRequestParts;
2use axum_core::__composite_rejection as composite_rejection;
3use axum_core::__define_rejection as define_rejection;
4use http::request::Parts;
5use serde::de::DeserializeOwned;
6
7#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
78#[derive(Debug, Clone, Copy, Default)]
79pub struct Query<T>(pub T);
80
81impl<T, S> FromRequestParts<S> for Query<T>
82where
83 T: DeserializeOwned,
84 S: Send + Sync,
85{
86 type Rejection = QueryRejection;
87
88 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
89 let query = parts.uri.query().unwrap_or_default();
90 let deserializer =
91 serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
92 let value = serde_path_to_error::deserialize(deserializer)
93 .map_err(FailedToDeserializeQueryString::from_err)?;
94 Ok(Query(value))
95 }
96}
97
98axum_core::__impl_deref!(Query);
99
100define_rejection! {
101 #[status = BAD_REQUEST]
102 #[body = "Failed to deserialize query string"]
103 pub struct FailedToDeserializeQueryString(Error);
106}
107
108composite_rejection! {
109 pub enum QueryRejection {
113 FailedToDeserializeQueryString,
114 }
115}
116
117#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
157#[derive(Debug, Clone, Copy, Default)]
158pub struct OptionalQuery<T>(pub Option<T>);
159
160impl<T, S> FromRequestParts<S> for OptionalQuery<T>
161where
162 T: DeserializeOwned,
163 S: Send + Sync,
164{
165 type Rejection = OptionalQueryRejection;
166
167 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
168 if let Some(query) = parts.uri.query() {
169 let deserializer =
170 serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
171 let value = serde_path_to_error::deserialize(deserializer)
172 .map_err(FailedToDeserializeQueryString::from_err)?;
173 Ok(OptionalQuery(Some(value)))
174 } else {
175 Ok(OptionalQuery(None))
176 }
177 }
178}
179
180impl<T> std::ops::Deref for OptionalQuery<T> {
181 type Target = Option<T>;
182
183 #[inline]
184 fn deref(&self) -> &Self::Target {
185 &self.0
186 }
187}
188
189impl<T> std::ops::DerefMut for OptionalQuery<T> {
190 #[inline]
191 fn deref_mut(&mut self) -> &mut Self::Target {
192 &mut self.0
193 }
194}
195
196composite_rejection! {
197 pub enum OptionalQueryRejection {
201 FailedToDeserializeQueryString,
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::test_helpers::*;
209 use axum::routing::{get, post};
210 use axum::Router;
211 use http::header::CONTENT_TYPE;
212 use http::StatusCode;
213 use serde::Deserialize;
214
215 #[tokio::test]
216 async fn query_supports_multiple_values() {
217 #[derive(Deserialize)]
218 struct Data {
219 #[serde(rename = "value")]
220 values: Vec<String>,
221 }
222
223 let app = Router::new().route(
224 "/",
225 post(|Query(data): Query<Data>| async move { data.values.join(",") }),
226 );
227
228 let client = TestClient::new(app);
229
230 let res = client
231 .post("/?value=one&value=two")
232 .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
233 .body("")
234 .await;
235
236 assert_eq!(res.status(), StatusCode::OK);
237 assert_eq!(res.text().await, "one,two");
238 }
239
240 #[tokio::test]
241 async fn correct_rejection_status_code() {
242 #[derive(Deserialize)]
243 #[allow(dead_code)]
244 struct Params {
245 n: i32,
246 }
247
248 async fn handler(_: Query<Params>) {}
249
250 let app = Router::new().route("/", get(handler));
251 let client = TestClient::new(app);
252
253 let res = client.get("/?n=hi").await;
254 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
255 assert_eq!(
256 res.text().await,
257 "Failed to deserialize query string: n: invalid digit found in string"
258 );
259 }
260
261 #[tokio::test]
262 async fn optional_query_supports_multiple_values() {
263 #[derive(Deserialize)]
264 struct Data {
265 #[serde(rename = "value")]
266 values: Vec<String>,
267 }
268
269 let app = Router::new().route(
270 "/",
271 post(|OptionalQuery(data): OptionalQuery<Data>| async move {
272 data.map(|Data { values }| values.join(","))
273 .unwrap_or("None".to_owned())
274 }),
275 );
276
277 let client = TestClient::new(app);
278
279 let res = client
280 .post("/?value=one&value=two")
281 .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
282 .body("")
283 .await;
284
285 assert_eq!(res.status(), StatusCode::OK);
286 assert_eq!(res.text().await, "one,two");
287 }
288
289 #[tokio::test]
290 async fn optional_query_deserializes_no_parameters_into_none() {
291 #[derive(Deserialize)]
292 struct Data {
293 value: String,
294 }
295
296 let app = Router::new().route(
297 "/",
298 post(|OptionalQuery(data): OptionalQuery<Data>| async move {
299 match data {
300 None => "None".into(),
301 Some(data) => data.value,
302 }
303 }),
304 );
305
306 let client = TestClient::new(app);
307
308 let res = client.post("/").body("").await;
309
310 assert_eq!(res.status(), StatusCode::OK);
311 assert_eq!(res.text().await, "None");
312 }
313
314 #[tokio::test]
315 async fn optional_query_preserves_parsing_errors() {
316 #[derive(Deserialize)]
317 struct Data {
318 value: String,
319 }
320
321 let app = Router::new().route(
322 "/",
323 post(|OptionalQuery(data): OptionalQuery<Data>| async move {
324 match data {
325 None => "None".into(),
326 Some(data) => data.value,
327 }
328 }),
329 );
330
331 let client = TestClient::new(app);
332
333 let res = client
334 .post("/?other=something")
335 .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
336 .body("")
337 .await;
338
339 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
340 }
341}