mirror of
https://github.com/tokio-rs/axum.git
synced 2024-10-24 01:46:51 +02:00
Use 422 Unprocessable Entity
for Form
deserialization errors, except GET
and HEAD
requests (#1683)
This commit is contained in:
parent
e6ff0281ae
commit
cd86f7ec7a
4 changed files with 71 additions and 41 deletions
|
@ -13,7 +13,7 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
|
||||||
(Response<()>, T1, R)
|
(Response<()>, T1, R)
|
||||||
(Response<()>, T1, T2, R)
|
(Response<()>, T1, T2, R)
|
||||||
(Response<()>, T1, T2, T3, R)
|
(Response<()>, T1, T2, T3, R)
|
||||||
and 124 others
|
and $N 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
|
||||||
|
|
|
|
||||||
|
|
|
@ -7,34 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
- None.
|
- **added:** Add `FormRejection::FailedToDeserializeFormBody` which is returned
|
||||||
|
if the request body couldn't be deserialized into the target type, as opposed
|
||||||
# 0.6.6 (12. February, 2023)
|
to `FailedToDeserializeForm` which is only for query parameters ([#1683])
|
||||||
|
|
||||||
- **fixed:** Enable passing `MethodRouter` to `Router::fallback` ([#1730])
|
|
||||||
|
|
||||||
[#1730]: https://github.com/tokio-rs/axum/pull/1730
|
|
||||||
|
|
||||||
# 0.6.5 (11. February, 2023)
|
|
||||||
|
|
||||||
- **fixed:** Fix `#[debug_handler]` sometimes giving wrong borrow related suggestions ([#1710])
|
|
||||||
- Document gotchas related to using `impl IntoResponse` as the return type from handler functions ([#1736])
|
|
||||||
|
|
||||||
[#1710]: https://github.com/tokio-rs/axum/pull/1710
|
|
||||||
[#1736]: https://github.com/tokio-rs/axum/pull/1736
|
|
||||||
|
|
||||||
# 0.6.4 (22. January, 2023)
|
|
||||||
|
|
||||||
- Depend on axum-macros 0.3.2
|
|
||||||
|
|
||||||
# 0.6.3 (20. January, 2023)
|
|
||||||
|
|
||||||
- **added:** Implement `IntoResponse` for `&'static [u8; N]` and `[u8; N]` ([#1690])
|
|
||||||
- **fixed:** Make `Path` support types using `serde::Deserializer::deserialize_any` ([#1693])
|
|
||||||
- **added:** Add `RawPathParams` ([#1713])
|
|
||||||
- **added:** Implement `Clone` and `Service` for `axum::middleware::Next` ([#1712])
|
|
||||||
- **fixed:** Document required tokio features to run "Hello, World!" example ([#1715])
|
|
||||||
|
|
||||||
|
[#1683]: https://github.com/tokio-rs/axum/pull/1683
|
||||||
[#1690]: https://github.com/tokio-rs/axum/pull/1690
|
[#1690]: https://github.com/tokio-rs/axum/pull/1690
|
||||||
[#1693]: https://github.com/tokio-rs/axum/pull/1693
|
[#1693]: https://github.com/tokio-rs/axum/pull/1693
|
||||||
[#1712]: https://github.com/tokio-rs/axum/pull/1712
|
[#1712]: https://github.com/tokio-rs/axum/pull/1712
|
||||||
|
|
|
@ -78,6 +78,14 @@ define_rejection! {
|
||||||
pub struct FailedToDeserializeForm(Error);
|
pub struct FailedToDeserializeForm(Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
define_rejection! {
|
||||||
|
#[status = UNPROCESSABLE_ENTITY]
|
||||||
|
#[body = "Failed to deserialize form body"]
|
||||||
|
/// Rejection type used if the [`Form`](super::Form) extractor is unable to
|
||||||
|
/// deserialize the form body into the target type.
|
||||||
|
pub struct FailedToDeserializeFormBody(Error);
|
||||||
|
}
|
||||||
|
|
||||||
define_rejection! {
|
define_rejection! {
|
||||||
#[status = BAD_REQUEST]
|
#[status = BAD_REQUEST]
|
||||||
#[body = "Failed to deserialize query string"]
|
#[body = "Failed to deserialize query string"]
|
||||||
|
@ -104,6 +112,7 @@ composite_rejection! {
|
||||||
pub enum FormRejection {
|
pub enum FormRejection {
|
||||||
InvalidFormContentType,
|
InvalidFormContentType,
|
||||||
FailedToDeserializeForm,
|
FailedToDeserializeForm,
|
||||||
|
FailedToDeserializeFormBody,
|
||||||
BytesRejection,
|
BytesRejection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,13 @@ use std::ops::Deref;
|
||||||
///
|
///
|
||||||
/// # As extractor
|
/// # As extractor
|
||||||
///
|
///
|
||||||
/// If used as an extractor `Form` will deserialize `application/x-www-form-urlencoded` request
|
/// If used as an extractor `Form` will deserialize the query parameters for `GET` and `HEAD`
|
||||||
/// bodies into some target type via [`serde::Deserialize`].
|
/// requests and `application/x-www-form-urlencoded` encoded request bodies for other methods. It
|
||||||
|
/// supports any type that implements [`serde::Deserialize`].
|
||||||
///
|
///
|
||||||
/// Since parsing form data requires consuming the request body, the `Form` extractor must be
|
/// Since parsing form data might require consuming the request body, the `Form` extractor must be
|
||||||
/// *last* if there are multiple extractors in a handler. See ["the order of extractors"][order-of-extractors]
|
/// *last* if there are multiple extractors in a handler. See ["the order of
|
||||||
|
/// extractors"][order-of-extractors]
|
||||||
///
|
///
|
||||||
/// [order-of-extractors]: crate::extract#the-order-of-extractors
|
/// [order-of-extractors]: crate::extract#the-order-of-extractors
|
||||||
///
|
///
|
||||||
|
@ -73,10 +75,19 @@ 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> {
|
||||||
|
let is_get_or_head =
|
||||||
|
req.method() == http::Method::GET || req.method() == http::Method::HEAD;
|
||||||
|
|
||||||
match req.extract().await {
|
match req.extract().await {
|
||||||
Ok(RawForm(bytes)) => {
|
Ok(RawForm(bytes)) => {
|
||||||
let value = serde_urlencoded::from_bytes(&bytes)
|
let value =
|
||||||
.map_err(FailedToDeserializeForm::from_err)?;
|
serde_urlencoded::from_bytes(&bytes).map_err(|err| -> FormRejection {
|
||||||
|
if is_get_or_head {
|
||||||
|
FailedToDeserializeForm::from_err(err).into()
|
||||||
|
} else {
|
||||||
|
FailedToDeserializeFormBody::from_err(err).into()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
Ok(Form(value))
|
Ok(Form(value))
|
||||||
}
|
}
|
||||||
Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)),
|
Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)),
|
||||||
|
@ -114,9 +125,15 @@ impl<T> Deref for Form<T> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::body::{Empty, Full};
|
use crate::{
|
||||||
|
body::{Empty, Full},
|
||||||
|
routing::{on, MethodFilter},
|
||||||
|
test_helpers::TestClient,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::{Method, Request};
|
use http::{header::CONTENT_TYPE, Method, Request};
|
||||||
|
use mime::APPLICATION_WWW_FORM_URLENCODED;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
@ -138,10 +155,7 @@ mod tests {
|
||||||
let req = Request::builder()
|
let req = Request::builder()
|
||||||
.uri("http://example.com/test")
|
.uri("http://example.com/test")
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.header(
|
.header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(),
|
|
||||||
)
|
|
||||||
.body(Full::<Bytes>::new(
|
.body(Full::<Bytes>::new(
|
||||||
serde_urlencoded::to_string(&value).unwrap().into(),
|
serde_urlencoded::to_string(&value).unwrap().into(),
|
||||||
))
|
))
|
||||||
|
@ -205,7 +219,7 @@ mod tests {
|
||||||
let req = Request::builder()
|
let req = Request::builder()
|
||||||
.uri("http://example.com/test")
|
.uri("http://example.com/test")
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
|
.header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
|
||||||
.body(Full::<Bytes>::new(
|
.body(Full::<Bytes>::new(
|
||||||
serde_urlencoded::to_string(&Pagination {
|
serde_urlencoded::to_string(&Pagination {
|
||||||
size: Some(10),
|
size: Some(10),
|
||||||
|
@ -222,4 +236,34 @@ mod tests {
|
||||||
FormRejection::InvalidFormContentType(InvalidFormContentType)
|
FormRejection::InvalidFormContentType(InvalidFormContentType)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn deserialize_error_status_codes() {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Payload {
|
||||||
|
a: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = Router::new().route(
|
||||||
|
"/",
|
||||||
|
on(
|
||||||
|
MethodFilter::GET | MethodFilter::POST,
|
||||||
|
|_: Form<Payload>| async {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = TestClient::new(app);
|
||||||
|
|
||||||
|
let res = client.get("/?a=false").send().await;
|
||||||
|
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.post("/")
|
||||||
|
.header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
|
||||||
|
.body("a=false")
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue