axum_extra/extract/
query.rs

1use 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/// Extractor that deserializes query strings into some type.
8///
9/// `T` is expected to implement [`serde::Deserialize`].
10///
11/// # Differences from `axum::extract::Query`
12///
13/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
14/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
15/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
16/// container.
17///
18/// # Example
19///
20/// ```rust,no_run
21/// use axum::{routing::get, Router};
22/// use axum_extra::extract::Query;
23/// use serde::Deserialize;
24///
25/// #[derive(Deserialize)]
26/// struct Pagination {
27///     page: usize,
28///     per_page: usize,
29/// }
30///
31/// // This will parse query strings like `?page=2&per_page=30` into `Pagination`
32/// // structs.
33/// async fn list_things(pagination: Query<Pagination>) {
34///     let pagination: Pagination = pagination.0;
35///
36///     // ...
37/// }
38///
39/// let app = Router::new().route("/list_things", get(list_things));
40/// # let _: Router = app;
41/// ```
42///
43/// If the query string cannot be parsed it will reject the request with a `400
44/// Bad Request` response.
45///
46/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
47/// example.
48///
49/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
50///
51/// While `Option<T>` will handle empty parameters (e.g. `param=`), beware when using this with a
52/// `Vec<T>`. If your list is optional, use `Vec<T>` in combination with `#[serde(default)]`
53/// instead of `Option<Vec<T>>`. `Option<Vec<T>>` will handle 0, 2, or more arguments, but not one
54/// argument.
55///
56/// # Example
57///
58/// ```rust,no_run
59/// use axum::{routing::get, Router};
60/// use axum_extra::extract::Query;
61/// use serde::Deserialize;
62///
63/// #[derive(Deserialize)]
64/// struct Params {
65///     #[serde(default)]
66///     items: Vec<usize>,
67/// }
68///
69/// // This will parse 0 occurrences of `items` as an empty `Vec`.
70/// async fn process_items(Query(params): Query<Params>) {
71///     // ...
72/// }
73///
74/// let app = Router::new().route("/process_items", get(process_items));
75/// # let _: Router = app;
76/// ```
77#[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    /// Rejection type used if the [`Query`] extractor is unable to
104    /// deserialize the query string into the target type.
105    pub struct FailedToDeserializeQueryString(Error);
106}
107
108composite_rejection! {
109    /// Rejection used for [`Query`].
110    ///
111    /// Contains one variant for each way the [`Query`] extractor can fail.
112    pub enum QueryRejection {
113        FailedToDeserializeQueryString,
114    }
115}
116
117/// Extractor that deserializes query strings into `None` if no query parameters are present.
118///
119/// Otherwise behaviour is identical to [`Query`].
120/// `T` is expected to implement [`serde::Deserialize`].
121///
122/// # Example
123///
124/// ```rust,no_run
125/// use axum::{routing::get, Router};
126/// use axum_extra::extract::OptionalQuery;
127/// use serde::Deserialize;
128///
129/// #[derive(Deserialize)]
130/// struct Pagination {
131///     page: usize,
132///     per_page: usize,
133/// }
134///
135/// // This will parse query strings like `?page=2&per_page=30` into `Some(Pagination)` and
136/// // empty query string into `None`
137/// async fn list_things(OptionalQuery(pagination): OptionalQuery<Pagination>) {
138///     match pagination {
139///         Some(Pagination{ page, per_page }) => { /* return specified page */ },
140///         None => { /* return fist page */ }
141///     }
142///     // ...
143/// }
144///
145/// let app = Router::new().route("/list_things", get(list_things));
146/// # let _: Router = app;
147/// ```
148///
149/// If the query string cannot be parsed it will reject the request with a `400
150/// Bad Request` response.
151///
152/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
153/// example.
154///
155/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
156#[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    /// Rejection used for [`OptionalQuery`].
198    ///
199    /// Contains one variant for each way the [`OptionalQuery`] extractor can fail.
200    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}