From 0bf2e300f2685b355410180545ee6598c1cf0c39 Mon Sep 17 00:00:00 2001 From: Vegard Sandengen Date: Mon, 22 Apr 2024 14:38:07 +0000 Subject: [PATCH] feat(extract::Path): add ErrorKind::DeserializeError to specialize ErrorKind::Message This commit introduces another `extract::path::ErrorKind` variant that captures the serde error nominally captured through the `serde::de::Error` trait impl on `PathDeserializeError`. We augment the deserialization error with the captured (key, value), allowing `extract::Path`, and wrapping extractors, to gain programmatic access to the key name, and attempted deserialized value. The `PathDeserializationError::custom` is used two places in addition to capture the deserialization error. These usages should still be unaffected. --- axum/src/extract/path/de.rs | 51 ++++++++++++++++++++++++++++++-- axum/src/extract/path/mod.rs | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/axum/src/extract/path/de.rs b/axum/src/extract/path/de.rs index 8ba8a431e9..c87062e16d 100644 --- a/axum/src/extract/path/de.rs +++ b/axum/src/extract/path/de.rs @@ -94,7 +94,20 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> { .got(self.url_params.len()) .expected(1)); } - visitor.visit_borrowed_str(&self.url_params[0].1) + let key = &self.url_params[0].0; + let value = &self.url_params[0].1; + visitor + .visit_borrowed_str(value) + .map_err(|e: Self::Error| match e.kind { + ErrorKind::Message(message) => { + PathDeserializationError::new(ErrorKind::DeserializeError { + key: key.to_string(), + value: value.as_str().to_owned(), + message, + }) + } + kind => PathDeserializationError::new(kind), + }) } fn deserialize_unit(self, visitor: V) -> Result @@ -362,7 +375,18 @@ impl<'de> Deserializer<'de> for ValueDeserializer<'de> { where V: Visitor<'de>, { - visitor.visit_borrowed_str(self.value) + visitor + .visit_borrowed_str(self.value) + .map_err(|e: Self::Error| match e.kind { + ErrorKind::Message(message) => { + PathDeserializationError::new(ErrorKind::DeserializeError { + key: self.key.map(|k| k.key().to_owned()).unwrap_or_default(), + value: self.value.as_str().to_owned(), + message, + }) + } + kind => PathDeserializationError::new(kind), + }) } fn deserialize_bytes(self, visitor: V) -> Result @@ -608,6 +632,15 @@ enum KeyOrIdx<'de> { Idx { idx: usize, key: &'de str }, } +impl<'de> KeyOrIdx<'de> { + fn key(&self) -> &'de str { + match &self { + Self::Key(key) => key, + Self::Idx { key, .. } => key, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -928,4 +961,18 @@ mod tests { } ); } + + #[test] + fn test_deserialize_key_value() { + test_parse_error!( + vec![("id", "123123-123-123123")], + uuid::Uuid, + ErrorKind::DeserializeError { + key: "id".to_string(), + value: "123123-123-123123".to_string(), + message: "UUID parsing failed: invalid group count: expected 5, found 3" + .to_string(), + } + ); + } } diff --git a/axum/src/extract/path/mod.rs b/axum/src/extract/path/mod.rs index 330e270ebc..ec8577e345 100644 --- a/axum/src/extract/path/mod.rs +++ b/axum/src/extract/path/mod.rs @@ -305,6 +305,16 @@ pub enum ErrorKind { name: &'static str, }, + /// Failed to deserialize the value with a custom deserialization error. + DeserializeError { + /// The key at which the invalid value was located. + key: String, + /// The value that failed to deserialize. + value: String, + /// The deserializaation failure message. + message: String, + }, + /// Catch-all variant for errors that don't fit any other variant. Message(String), } @@ -347,6 +357,11 @@ impl fmt::Display for ErrorKind { f, "Cannot parse value at index {index} with value `{value:?}` to a `{expected_type}`" ), + ErrorKind::DeserializeError { + key, + value, + message, + } => write!(f, "Cannot parse `{key}` with value `{value:?}`: {message}"), } } } @@ -371,6 +386,7 @@ impl FailedToDeserializePathParams { pub fn body_text(&self) -> String { match self.0.kind { ErrorKind::Message(_) + | ErrorKind::DeserializeError { .. } | ErrorKind::InvalidUtf8InPathParam { .. } | ErrorKind::ParseError { .. } | ErrorKind::ParseErrorAtIndex { .. } @@ -385,6 +401,7 @@ impl FailedToDeserializePathParams { pub fn status(&self) -> StatusCode { match self.0.kind { ErrorKind::Message(_) + | ErrorKind::DeserializeError { .. } | ErrorKind::InvalidUtf8InPathParam { .. } | ErrorKind::ParseError { .. } | ErrorKind::ParseErrorAtIndex { .. } @@ -887,4 +904,43 @@ mod tests { let body = res.text().await; assert_eq!(body, "a=foo b=bar c=baz"); } + + #[crate::test] + async fn deserialize_error_single_value() { + let app = Router::new().route( + "/resources/:res", + get(|res: Path| async move { + let _res = res; + }), + ); + + let client = TestClient::new(app); + let res = client.get("/resources/123123-123-123123").await; + let body = res.text().await; + assert_eq!( + body, + r#"Invalid URL: Cannot parse `res` with value `"123123-123-123123"`: UUID parsing failed: invalid group count: expected 5, found 3"# + ); + } + + #[crate::test] + async fn deserialize_error_multi_value() { + let app = Router::new().route( + "/resources/:res/sub/:sub", + get( + |Path((res, sub)): Path<(uuid::Uuid, uuid::Uuid)>| async move { + let _res = res; + let _sub = sub; + }, + ), + ); + + let client = TestClient::new(app); + let res = client.get("/resources/456456-123-456456/sub/123").await; + let body = res.text().await; + assert_eq!( + body, + r#"Invalid URL: Cannot parse `res` with value `"456456-123-456456"`: UUID parsing failed: invalid group count: expected 5, found 3"# + ); + } }