mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-28 00:00:20 +02:00
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:
@@ -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)]
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)),
|
||||||
|
|||||||
Reference in New Issue
Block a user