#![allow(deprecated)] use axum_core::__composite_rejection as composite_rejection; use axum_core::__define_rejection as define_rejection; use axum_core::extract::FromRequestParts; use http::{request::Parts, Uri}; use serde_core::de::DeserializeOwned; /// Extractor that deserializes query strings into some type. /// /// `T` is expected to implement [`serde::Deserialize`]. /// /// # Deprecated /// /// This extractor used to use a different deserializer under-the-hood but that /// is no longer the case. Now it only uses an older version of the same /// deserializer, purely for ease of transition to the latest version. /// Before switching to `axum::extract::Form`, it is recommended to read the /// [changelog for `serde_html_form v0.3.0`][changelog]. /// /// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030 #[deprecated = "see documentation"] #[cfg_attr(docsrs, doc(cfg(feature = "query")))] #[derive(Debug, Clone, Copy, Default)] pub struct Query(pub T); impl FromRequestParts for Query where T: DeserializeOwned, S: Send + Sync, { type Rejection = QueryRejection; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let query = parts.uri.query().unwrap_or_default(); let deserializer = serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); let value = serde_path_to_error::deserialize(deserializer) .map_err(FailedToDeserializeQueryString::from_err)?; Ok(Self(value)) } } impl Query where T: DeserializeOwned, { /// Attempts to construct a [`Query`] from a reference to a [`Uri`]. /// /// # Example /// ``` /// use axum_extra::extract::Query; /// use http::Uri; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct ExampleParams { /// foo: String, /// bar: u32, /// } /// /// let uri: Uri = "http://example.com/path?foo=hello&bar=42".parse().unwrap(); /// let result: Query = Query::try_from_uri(&uri).unwrap(); /// assert_eq!(result.foo, String::from("hello")); /// assert_eq!(result.bar, 42); /// ``` pub fn try_from_uri(value: &Uri) -> Result { let query = value.query().unwrap_or_default(); let params = serde_html_form::from_str(query).map_err(FailedToDeserializeQueryString::from_err)?; Ok(Self(params)) } } axum_core::__impl_deref!(Query); define_rejection! { #[status = BAD_REQUEST] #[body = "Failed to deserialize query string"] /// Rejection type used if the [`Query`] extractor is unable to /// deserialize the query string into the target type. pub struct FailedToDeserializeQueryString(Error); } composite_rejection! { /// Rejection used for [`Query`]. /// /// Contains one variant for each way the [`Query`] extractor can fail. #[deprecated = "because Query is deprecated"] pub enum QueryRejection { FailedToDeserializeQueryString, } } /// Extractor that deserializes query strings into `None` if no query parameters are present. /// /// Otherwise behaviour is identical to [`Query`][axum::extract::Query]. /// `T` is expected to implement [`serde::Deserialize`]. /// /// # Example /// /// ```rust,no_run /// use axum::{routing::get, Router}; /// use axum_extra::extract::OptionalQuery; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Pagination { /// page: usize, /// per_page: usize, /// } /// /// // This will parse query strings like `?page=2&per_page=30` into `Some(Pagination)` and /// // empty query string into `None` /// async fn list_things(OptionalQuery(pagination): OptionalQuery) { /// match pagination { /// Some(Pagination{ page, per_page }) => { /* return specified page */ }, /// None => { /* return fist page */ } /// } /// // ... /// } /// /// let app = Router::new().route("/list_things", get(list_things)); /// # let _: Router = app; /// ``` /// /// If the query string cannot be parsed it will reject the request with a `400 /// Bad Request` response. #[cfg_attr(docsrs, doc(cfg(feature = "query")))] #[derive(Debug, Clone, Copy, Default)] pub struct OptionalQuery(pub Option); impl FromRequestParts for OptionalQuery where T: DeserializeOwned, S: Send + Sync, { type Rejection = OptionalQueryRejection; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { if let Some(query) = parts.uri.query() { let deserializer = serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); let value = serde_path_to_error::deserialize(deserializer) .map_err(FailedToDeserializeQueryString::from_err)?; Ok(Self(Some(value))) } else { Ok(Self(None)) } } } impl std::ops::Deref for OptionalQuery { type Target = Option; #[inline] fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for OptionalQuery { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } composite_rejection! { /// Rejection used for [`OptionalQuery`]. /// /// Contains one variant for each way the [`OptionalQuery`] extractor can fail. pub enum OptionalQueryRejection { FailedToDeserializeQueryString, } } #[cfg(test)] mod tests { use super::*; use crate::test_helpers::*; use axum::routing::{get, post}; use axum::Router; use http::header::CONTENT_TYPE; use http::StatusCode; use serde::Deserialize; #[tokio::test] async fn query_supports_multiple_values() { #[derive(Deserialize)] struct Data { #[serde(rename = "value")] values: Vec, } let app = Router::new().route( "/", post(|Query(data): Query| async move { data.values.join(",") }), ); let client = TestClient::new(app); let res = client .post("/?value=one&value=two") .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body("") .await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.text().await, "one,two"); } #[tokio::test] async fn correct_rejection_status_code() { #[derive(Deserialize)] #[allow(dead_code)] struct Params { n: i32, } async fn handler(_: Query) {} let app = Router::new().route("/", get(handler)); let client = TestClient::new(app); let res = client.get("/?n=hi").await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( res.text().await, "Failed to deserialize query string: n: invalid digit found in string" ); } #[tokio::test] async fn optional_query_supports_multiple_values() { #[derive(Deserialize)] struct Data { #[serde(rename = "value")] values: Vec, } let app = Router::new().route( "/", post(|OptionalQuery(data): OptionalQuery| async move { data.map(|Data { values }| values.join(",")) .unwrap_or("None".to_owned()) }), ); let client = TestClient::new(app); let res = client .post("/?value=one&value=two") .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body("") .await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.text().await, "one,two"); } #[tokio::test] async fn optional_query_deserializes_no_parameters_into_none() { #[derive(Deserialize)] struct Data { value: String, } let app = Router::new().route( "/", post(|OptionalQuery(data): OptionalQuery| async move { match data { None => "None".into(), Some(data) => data.value, } }), ); let client = TestClient::new(app); let res = client.post("/").body("").await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.text().await, "None"); } #[tokio::test] async fn optional_query_preserves_parsing_errors() { #[derive(Deserialize)] struct Data { value: String, } let app = Router::new().route( "/", post(|OptionalQuery(data): OptionalQuery| async move { match data { None => "None".into(), Some(data) => data.value, } }), ); let client = TestClient::new(app); let res = client .post("/?other=something") .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body("") .await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[test] fn test_try_from_uri() { #[derive(Deserialize)] struct TestQueryParams { foo: Vec, bar: u32, } let uri: Uri = "http://example.com/path?foo=hello&bar=42&foo=goodbye" .parse() .unwrap(); let result: Query = Query::try_from_uri(&uri).unwrap(); assert_eq!(result.foo, [String::from("hello"), String::from("goodbye")]); assert_eq!(result.bar, 42); } #[test] fn test_try_from_uri_with_invalid_query() { #[derive(Deserialize)] struct TestQueryParams { _foo: String, _bar: u32, } let uri: Uri = "http://example.com/path?foo=hello&bar=invalid" .parse() .unwrap(); let result: Result, _> = Query::try_from_uri(&uri); assert!(result.is_err()); } }