Rework Form and Query rejections (#1496)

* Change `FailedToDeserializeQueryString` rejection for `Form`

Its now called `FailedToDeserializeForm`.

* changelog

* Make dedicate rejection type for axum-extra's `Form`

* update trybuild test

* Make dedicate rejection type for axum-extra's `Query`
This commit is contained in:
David Pedersen
2022-11-08 20:31:06 +00:00
committed by GitHub
parent 8d6313afa0
commit e0ef641e5f
8 changed files with 119 additions and 82 deletions
+52 -36
View File
@@ -1,16 +1,13 @@
use axum::{ use axum::{
async_trait, async_trait,
body::HttpBody, body::HttpBody,
extract::{ extract::{rejection::RawFormRejection, FromRequest, RawForm},
rejection::{FailedToDeserializeQueryString, FormRejection, InvalidFormContentType}, response::{IntoResponse, Response},
FromRequest, BoxError, Error, RequestExt,
},
BoxError,
}; };
use bytes::Bytes; use http::{Request, StatusCode};
use http::{header, HeaderMap, Method, Request};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::ops::Deref; use std::{fmt, ops::Deref};
/// Extractor that deserializes `application/x-www-form-urlencoded` requests /// Extractor that deserializes `application/x-www-form-urlencoded` requests
/// into some type. /// into some type.
@@ -65,41 +62,60 @@ where
{ {
type Rejection = FormRejection; type Rejection = FormRejection;
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> { async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> {
if req.method() == Method::GET { let RawForm(bytes) = req
let query = req.uri().query().unwrap_or_default(); .extract()
let value = serde_html_form::from_str(query) .await
.map_err(FailedToDeserializeQueryString::__private_new)?; .map_err(FormRejection::RawFormRejection)?;
Ok(Form(value))
} else {
if !has_content_type(req.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) {
return Err(InvalidFormContentType::default().into());
}
let bytes = Bytes::from_request(req, state).await?; serde_html_form::from_bytes::<T>(&bytes)
let value = serde_html_form::from_bytes(&bytes) .map(Self)
.map_err(FailedToDeserializeQueryString::__private_new)?; .map_err(|err| FormRejection::FailedToDeserializeForm(Error::new(err)))
}
}
Ok(Form(value)) /// Rejection used for [`Form`].
///
/// Contains one variant for each way the [`Form`] extractor can fail.
#[derive(Debug)]
#[non_exhaustive]
#[cfg(feature = "form")]
pub enum FormRejection {
#[allow(missing_docs)]
RawFormRejection(RawFormRejection),
#[allow(missing_docs)]
FailedToDeserializeForm(Error),
}
impl IntoResponse for FormRejection {
fn into_response(self) -> Response {
match self {
Self::RawFormRejection(inner) => inner.into_response(),
Self::FailedToDeserializeForm(inner) => (
StatusCode::BAD_REQUEST,
format!("Failed to deserialize form: {}", inner),
)
.into_response(),
} }
} }
} }
// this is duplicated in `axum/src/extract/mod.rs` impl fmt::Display for FormRejection {
fn has_content_type(headers: &HeaderMap, expected_content_type: &mime::Mime) -> bool { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { match self {
content_type Self::RawFormRejection(inner) => inner.fmt(f),
} else { Self::FailedToDeserializeForm(inner) => inner.fmt(f),
return false; }
}; }
}
let content_type = if let Ok(content_type) = content_type.to_str() { impl std::error::Error for FormRejection {
content_type fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
} else { match self {
return false; Self::RawFormRejection(inner) => Some(inner),
}; Self::FailedToDeserializeForm(inner) => Some(inner),
}
content_type.starts_with(expected_content_type.as_ref()) }
} }
#[cfg(test)] #[cfg(test)]
+2 -2
View File
@@ -25,10 +25,10 @@ pub use self::cookie::PrivateCookieJar;
pub use self::cookie::SignedCookieJar; pub use self::cookie::SignedCookieJar;
#[cfg(feature = "form")] #[cfg(feature = "form")]
pub use self::form::Form; pub use self::form::{Form, FormRejection};
#[cfg(feature = "query")] #[cfg(feature = "query")]
pub use self::query::Query; pub use self::query::{Query, QueryRejection};
#[cfg(feature = "json-lines")] #[cfg(feature = "json-lines")]
#[doc(no_inline)] #[doc(no_inline)]
+45 -7
View File
@@ -1,13 +1,12 @@
use axum::{ use axum::{
async_trait, async_trait,
extract::{ extract::FromRequestParts,
rejection::{FailedToDeserializeQueryString, QueryRejection}, response::{IntoResponse, Response},
FromRequestParts, Error,
},
}; };
use http::request::Parts; use http::{request::Parts, StatusCode};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::ops::Deref; use std::{fmt, ops::Deref};
/// Extractor that deserializes query strings into some type. /// Extractor that deserializes query strings into some type.
/// ///
@@ -69,7 +68,7 @@ where
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let query = parts.uri.query().unwrap_or_default(); let query = parts.uri.query().unwrap_or_default();
let value = serde_html_form::from_str(query) let value = serde_html_form::from_str(query)
.map_err(FailedToDeserializeQueryString::__private_new)?; .map_err(|err| QueryRejection::FailedToDeserializeQueryString(Error::new(err)))?;
Ok(Query(value)) Ok(Query(value))
} }
} }
@@ -82,6 +81,45 @@ impl<T> Deref for Query<T> {
} }
} }
/// Rejection used for [`Query`].
///
/// Contains one variant for each way the [`Query`] extractor can fail.
#[derive(Debug)]
#[non_exhaustive]
#[cfg(feature = "query")]
pub enum QueryRejection {
#[allow(missing_docs)]
FailedToDeserializeQueryString(Error),
}
impl IntoResponse for QueryRejection {
fn into_response(self) -> Response {
match self {
Self::FailedToDeserializeQueryString(inner) => (
StatusCode::BAD_REQUEST,
format!("Failed to deserialize query string: {}", inner),
)
.into_response(),
}
}
}
impl fmt::Display for QueryRejection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FailedToDeserializeQueryString(inner) => inner.fmt(f),
}
}
}
impl std::error::Error for QueryRejection {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::FailedToDeserializeQueryString(inner) => Some(inner),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -13,7 +13,7 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
(Response<()>, T1, T2, R) (Response<()>, T1, T2, R)
(Response<()>, T1, T2, T3, R) (Response<()>, T1, T2, T3, R)
(Response<()>, T1, T2, T3, T4, R) (Response<()>, T1, T2, T3, T4, R)
and 119 others and 120 others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check` note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/wrong_return_type.rs:4:23 --> tests/debug_handler/fail/wrong_return_type.rs:4:23
| |
+3
View File
@@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **added:** `FromRequest` and `FromRequestParts` derive macro re-exports from - **added:** `FromRequest` and `FromRequestParts` derive macro re-exports from
[`axum-macros`] behind the `macros` feature ([#1352]) [`axum-macros`] behind the `macros` feature ([#1352])
- **added:** Add `extract::RawForm` for accessing raw urlencoded query bytes or request body ([#1487]) - **added:** Add `extract::RawForm` for accessing raw urlencoded query bytes or request body ([#1487])
- **breaking:** Rename `FormRejection::FailedToDeserializeQueryString` to
`FormRejection::FailedToDeserializeForm` ([#1496])
[#1352]: https://github.com/tokio-rs/axum/pull/1352 [#1352]: https://github.com/tokio-rs/axum/pull/1352
[#1368]: https://github.com/tokio-rs/axum/pull/1368 [#1368]: https://github.com/tokio-rs/axum/pull/1368
@@ -63,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1420]: https://github.com/tokio-rs/axum/pull/1420 [#1420]: https://github.com/tokio-rs/axum/pull/1420
[#1421]: https://github.com/tokio-rs/axum/pull/1421 [#1421]: https://github.com/tokio-rs/axum/pull/1421
[#1487]: https://github.com/tokio-rs/axum/pull/1487 [#1487]: https://github.com/tokio-rs/axum/pull/1487
[#1496]: https://github.com/tokio-rs/axum/pull/1496
# 0.6.0-rc.2 (10. September, 2022) # 0.6.0-rc.2 (10. September, 2022)
+2 -2
View File
@@ -59,8 +59,8 @@ where
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let query = parts.uri.query().unwrap_or_default(); let query = parts.uri.query().unwrap_or_default();
let value = serde_urlencoded::from_str(query) let value =
.map_err(FailedToDeserializeQueryString::__private_new)?; serde_urlencoded::from_str(query).map_err(FailedToDeserializeQueryString::from_err)?;
Ok(Query(value)) Ok(Query(value))
} }
} }
+13 -33
View File
@@ -1,8 +1,5 @@
//! Rejection response types. //! Rejection response types.
use crate::{BoxError, Error};
use axum_core::response::{IntoResponse, Response};
pub use crate::extract::path::FailedToDeserializePathParams; pub use crate::extract::path::FailedToDeserializePathParams;
pub use axum_core::extract::rejection::*; pub use axum_core::extract::rejection::*;
@@ -73,39 +70,22 @@ define_rejection! {
pub struct FailedToResolveHost; pub struct FailedToResolveHost;
} }
/// Rejection type for extractors that deserialize query strings if the input define_rejection! {
/// couldn't be deserialized into the target type. #[status = BAD_REQUEST]
#[derive(Debug)] #[body = "Failed to deserialize form"]
pub struct FailedToDeserializeQueryString { /// Rejection type used if the [`Form`](super::Form) extractor is unable to
error: Error, /// deserialize the form into the target type.
pub struct FailedToDeserializeForm(Error);
} }
impl FailedToDeserializeQueryString { define_rejection! {
#[doc(hidden)] #[status = BAD_REQUEST]
pub fn __private_new<E>(error: E) -> Self #[body = "Failed to deserialize query string"]
where /// Rejection type used if the [`Query`](super::Query) extractor is unable to
E: Into<BoxError>, /// deserialize the form into the target type.
{ pub struct FailedToDeserializeQueryString(Error);
FailedToDeserializeQueryString {
error: Error::new(error),
}
}
} }
impl IntoResponse for FailedToDeserializeQueryString {
fn into_response(self) -> Response {
(http::StatusCode::BAD_REQUEST, self.to_string()).into_response()
}
}
impl std::fmt::Display for FailedToDeserializeQueryString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to deserialize query string: {}", self.error,)
}
}
impl std::error::Error for FailedToDeserializeQueryString {}
composite_rejection! { composite_rejection! {
/// Rejection used for [`Query`](super::Query). /// Rejection used for [`Query`](super::Query).
/// ///
@@ -123,7 +103,7 @@ composite_rejection! {
/// can fail. /// can fail.
pub enum FormRejection { pub enum FormRejection {
InvalidFormContentType, InvalidFormContentType,
FailedToDeserializeQueryString, FailedToDeserializeForm,
BytesRejection, BytesRejection,
} }
} }
+1 -1
View File
@@ -76,7 +76,7 @@ where
match req.extract().await { match req.extract().await {
Ok(RawForm(bytes)) => { Ok(RawForm(bytes)) => {
let value = serde_urlencoded::from_bytes(&bytes) let value = serde_urlencoded::from_bytes(&bytes)
.map_err(FailedToDeserializeQueryString::__private_new)?; .map_err(FailedToDeserializeForm::from_err)?;
Ok(Form(value)) Ok(Form(value))
} }
Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)), Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)),