From bb8e02cdccc60a289e2206cfeb4bdc95f065060f Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 18 Dec 2024 17:26:37 +0100 Subject: [PATCH 1/9] axum/extract/query: Add rejection message assertion --- axum/src/extract/query.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/axum/src/extract/query.rs b/axum/src/extract/query.rs index 14473aab04..35fbe5c149 100644 --- a/axum/src/extract/query.rs +++ b/axum/src/extract/query.rs @@ -201,6 +201,10 @@ mod tests { let res = client.get("/?n=hi").await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!( + res.text().await, + "Failed to deserialize query string: invalid digit found in string" + ); } #[test] From 42d2ba38934322c50bf39c806af8f142fca1dee2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 18 Dec 2024 17:30:14 +0100 Subject: [PATCH 2/9] axum/extract/query: Use `serde_path_to_error` to report key that failed to parse --- axum/Cargo.toml | 3 ++- axum/src/extract/query.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/axum/Cargo.toml b/axum/Cargo.toml index fb88399ec5..0955ec2428 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -31,7 +31,7 @@ macros = ["dep:axum-macros"] matched-path = [] multipart = ["dep:multer"] original-uri = [] -query = ["dep:serde_urlencoded"] +query = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"] tokio = ["dep:hyper-util", "dep:tokio", "tokio/net", "tokio/rt", "tower/make", "tokio/macros"] tower-log = ["tower/log"] tracing = ["dep:tracing", "axum-core/tracing"] @@ -68,6 +68,7 @@ tower-service = "0.3" # optional dependencies axum-macros = { path = "../axum-macros", version = "0.5.0-rc.1", optional = true } base64 = { version = "0.22.1", optional = true } +form_urlencoded = { version = "1.1.0", optional = true } hyper = { version = "1.1.0", optional = true } hyper-util = { version = "0.1.3", features = ["tokio", "server", "service"], optional = true } multer = { version = "3.0.0", optional = true } diff --git a/axum/src/extract/query.rs b/axum/src/extract/query.rs index 35fbe5c149..64221afabb 100644 --- a/axum/src/extract/query.rs +++ b/axum/src/extract/query.rs @@ -87,7 +87,9 @@ where _state: &S, ) -> Result, Self::Rejection> { if let Some(query) = parts.uri.query() { - let value = serde_urlencoded::from_str(query) + let deserializer = + serde_urlencoded::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + let value = serde_path_to_error::deserialize(deserializer) .map_err(FailedToDeserializeQueryString::from_err)?; Ok(Some(Self(value))) } else { @@ -121,8 +123,10 @@ where /// ``` pub fn try_from_uri(value: &Uri) -> Result { let query = value.query().unwrap_or_default(); - let params = - serde_urlencoded::from_str(query).map_err(FailedToDeserializeQueryString::from_err)?; + let deserializer = + serde_urlencoded::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + let params = serde_path_to_error::deserialize(deserializer) + .map_err(FailedToDeserializeQueryString::from_err)?; Ok(Query(params)) } } @@ -203,7 +207,7 @@ mod tests { assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( res.text().await, - "Failed to deserialize query string: invalid digit found in string" + "Failed to deserialize query string: n: invalid digit found in string" ); } From 032d581f295b501d61d9e9568f326c8e5c7e587a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 20 Dec 2024 09:20:23 +0100 Subject: [PATCH 3/9] axum/form: Add rejection message assertions --- axum/src/form.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/axum/src/form.rs b/axum/src/form.rs index f754c4c1b8..8fde42889d 100644 --- a/axum/src/form.rs +++ b/axum/src/form.rs @@ -252,6 +252,10 @@ mod tests { let res = client.get("/?a=false").await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!( + res.text().await, + "Failed to deserialize form: invalid digit found in string" + ); let res = client .post("/") @@ -259,5 +263,9 @@ mod tests { .body("a=false") .await; assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + assert_eq!( + res.text().await, + "Failed to deserialize form body: invalid digit found in string" + ); } } From 03372182c56b509c411bfb43331dd9a79bb47166 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 20 Dec 2024 09:22:47 +0100 Subject: [PATCH 4/9] axum/form: Use `serde_path_to_error` to report key that failed to parse --- axum/Cargo.toml | 2 +- axum/src/form.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/axum/Cargo.toml b/axum/Cargo.toml index 0955ec2428..833d935266 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -23,7 +23,7 @@ default = [ "tower-log", "tracing", ] -form = ["dep:serde_urlencoded"] +form = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"] http1 = ["dep:hyper", "hyper?/http1", "hyper-util?/http1"] http2 = ["dep:hyper", "hyper?/http2", "hyper-util?/http2"] json = ["dep:serde_json", "dep:serde_path_to_error"] diff --git a/axum/src/form.rs b/axum/src/form.rs index 8fde42889d..fd7c033815 100644 --- a/axum/src/form.rs +++ b/axum/src/form.rs @@ -84,14 +84,17 @@ where match req.extract().await { Ok(RawForm(bytes)) => { - let value = - serde_urlencoded::from_bytes(&bytes).map_err(|err| -> FormRejection { + let deserializer = + serde_urlencoded::Deserializer::new(form_urlencoded::parse(&bytes)); + let value = serde_path_to_error::deserialize(deserializer).map_err( + |err| -> FormRejection { if is_get_or_head { FailedToDeserializeForm::from_err(err).into() } else { FailedToDeserializeFormBody::from_err(err).into() } - })?; + }, + )?; Ok(Form(value)) } Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)), @@ -254,7 +257,7 @@ mod tests { assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( res.text().await, - "Failed to deserialize form: invalid digit found in string" + "Failed to deserialize form: a: invalid digit found in string" ); let res = client @@ -265,7 +268,7 @@ mod tests { assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); assert_eq!( res.text().await, - "Failed to deserialize form body: invalid digit found in string" + "Failed to deserialize form body: a: invalid digit found in string" ); } } From afb1873c1f8a28a04b8e48ab202716d526b76b70 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 20 Dec 2024 09:25:35 +0100 Subject: [PATCH 5/9] axum-extra/extract/query: Add rejection test case --- axum-extra/src/extract/query.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/axum-extra/src/extract/query.rs b/axum-extra/src/extract/query.rs index 6e50456e2f..59dbdaee07 100644 --- a/axum-extra/src/extract/query.rs +++ b/axum-extra/src/extract/query.rs @@ -302,7 +302,8 @@ impl std::error::Error for OptionalQueryRejection { mod tests { use super::*; use crate::test_helpers::*; - use axum::{routing::post, Router}; + use axum::routing::{get, post}; + use axum::Router; use http::header::CONTENT_TYPE; use serde::Deserialize; @@ -331,6 +332,27 @@ mod tests { 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: invalid digit found in string" + ); + } + #[tokio::test] async fn optional_query_supports_multiple_values() { #[derive(Deserialize)] From 7cb81961c4a3c1f1b361c5155c81dfe6040c153f Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 20 Dec 2024 09:33:16 +0100 Subject: [PATCH 6/9] axum-extra/extract/query: Use `serde_path_to_error` to report key that failed to parse --- axum-extra/Cargo.toml | 2 +- axum-extra/src/extract/query.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index d8f5435fd3..a51d65474e 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -36,7 +36,7 @@ json-lines = [ multipart = ["dep:multer", "dep:fastrand"] protobuf = ["dep:prost"] scheme = [] -query = ["dep:serde_html_form"] +query = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"] tracing = ["axum-core/tracing", "axum/tracing"] typed-header = ["dep:headers"] typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"] diff --git a/axum-extra/src/extract/query.rs b/axum-extra/src/extract/query.rs index 59dbdaee07..489fc1c7d4 100644 --- a/axum-extra/src/extract/query.rs +++ b/axum-extra/src/extract/query.rs @@ -103,7 +103,9 @@ where async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let query = parts.uri.query().unwrap_or_default(); - let value = serde_html_form::from_str(query) + let deserializer = + serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + let value = serde_path_to_error::deserialize(deserializer) .map_err(|err| QueryRejection::FailedToDeserializeQueryString(Error::new(err)))?; Ok(Query(value)) } @@ -121,7 +123,9 @@ where _state: &S, ) -> Result, Self::Rejection> { if let Some(query) = parts.uri.query() { - let value = serde_html_form::from_str(query) + let deserializer = + serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + let value = serde_path_to_error::deserialize(deserializer) .map_err(|err| QueryRejection::FailedToDeserializeQueryString(Error::new(err)))?; Ok(Some(Self(value))) } else { @@ -230,7 +234,9 @@ where async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { if let Some(query) = parts.uri.query() { - let value = serde_html_form::from_str(query).map_err(|err| { + let deserializer = + serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + let value = serde_path_to_error::deserialize(deserializer).map_err(|err| { OptionalQueryRejection::FailedToDeserializeQueryString(Error::new(err)) })?; Ok(OptionalQuery(Some(value))) @@ -349,7 +355,7 @@ mod tests { assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( res.text().await, - "Failed to deserialize query string: invalid digit found in string" + "Failed to deserialize query string: n: invalid digit found in string" ); } From 7e567c31eb0f5e5ff6f386d397497dd37f4a21bc Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 20 Dec 2024 09:35:53 +0100 Subject: [PATCH 7/9] axum-extra/extract/form: Add rejection test case --- axum-extra/src/extract/form.rs | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/axum-extra/src/extract/form.rs b/axum-extra/src/extract/form.rs index a7ca9305aa..33b8f7ec63 100644 --- a/axum-extra/src/extract/form.rs +++ b/axum-extra/src/extract/form.rs @@ -115,8 +115,10 @@ impl std::error::Error for FormRejection { mod tests { use super::*; use crate::test_helpers::*; - use axum::{routing::post, Router}; + use axum::routing::{on, post, MethodFilter}; + use axum::Router; use http::header::CONTENT_TYPE; + use mime::APPLICATION_WWW_FORM_URLENCODED; use serde::Deserialize; #[tokio::test] @@ -143,4 +145,41 @@ mod tests { assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.text().await, "one,two"); } + + #[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.or(MethodFilter::POST), + |_: Form| async {}, + ), + ); + + let client = TestClient::new(app); + + let res = client.get("/?a=false").await; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!( + res.text().await, + "Failed to deserialize form: invalid digit found in string" + ); + + let res = client + .post("/") + .header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref()) + .body("a=false") + .await; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!( + res.text().await, + "Failed to deserialize form: invalid digit found in string" + ); + } } From 08864bdc4344497cd836100683cfb5700c1c1dff Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 20 Dec 2024 09:37:38 +0100 Subject: [PATCH 8/9] axum-extra/extract/form: Use `serde_path_to_error` to report key that failed to parse --- axum-extra/Cargo.toml | 2 +- axum-extra/src/extract/form.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index a51d65474e..384544568e 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -23,7 +23,7 @@ cookie-private = ["cookie", "cookie?/private"] cookie-signed = ["cookie", "cookie?/signed"] cookie-key-expansion = ["cookie", "cookie?/key-expansion"] erased-json = ["dep:serde_json", "dep:typed-json"] -form = ["dep:serde_html_form"] +form = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"] json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"] json-lines = [ "dep:serde_json", diff --git a/axum-extra/src/extract/form.rs b/axum-extra/src/extract/form.rs index 33b8f7ec63..8d2d30f91c 100644 --- a/axum-extra/src/extract/form.rs +++ b/axum-extra/src/extract/form.rs @@ -56,7 +56,9 @@ where .await .map_err(FormRejection::RawFormRejection)?; - serde_html_form::from_bytes::(&bytes) + let deserializer = serde_html_form::Deserializer::new(form_urlencoded::parse(&bytes)); + + serde_path_to_error::deserialize::<_, T>(deserializer) .map(Self) .map_err(|err| FormRejection::FailedToDeserializeForm(Error::new(err))) } @@ -168,7 +170,7 @@ mod tests { assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( res.text().await, - "Failed to deserialize form: invalid digit found in string" + "Failed to deserialize form: a: invalid digit found in string" ); let res = client @@ -179,7 +181,7 @@ mod tests { assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( res.text().await, - "Failed to deserialize form: invalid digit found in string" + "Failed to deserialize form: a: invalid digit found in string" ); } } From 6102e666dcc5e6c94bc90e66c636ea54ae4db8c2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 20 Dec 2024 09:39:27 +0100 Subject: [PATCH 9/9] Add changelog entries --- axum-extra/CHANGELOG.md | 2 ++ axum/CHANGELOG.md | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index a58aec6c1e..0a1f7523e6 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -10,8 +10,10 @@ and this project adheres to [Semantic Versioning]. - **breaking:** `axum::extract::ws::Message` now uses `Bytes` in place of `Vec`, and a new `Utf8Bytes` type in place of `String`, for its variants ([#3078]) - **changed:** Upgraded `tokio-tungstenite` to 0.26 ([#3078]) +- **changed:** Query/Form: Use `serde_path_to_error` to report fields that failed to parse ([#3081]) [#3078]: https://github.com/tokio-rs/axum/pull/3078 +[#3081]: https://github.com/tokio-rs/axum/pull/3081 # 0.10.0 diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index e896deb317..5301ebf51a 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased +- **changed:** Query/Form: Use `serde_path_to_error` to report fields that failed to parse ([#3081]) + +[#3081]: https://github.com/tokio-rs/axum/pull/3081 + # 0.8.0 ## rc.1