Use 422 Unprocessable Entity for Form deserialization errors, except GET and HEAD requests (#1683)

This commit is contained in:
David Pedersen 2023-02-17 09:59:02 +01:00 committed by GitHub
parent e6ff0281ae
commit cd86f7ec7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 41 deletions

View file

@ -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
| |

View file

@ -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

View file

@ -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,
} }
} }

View file

@ -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);
}
} }