Add ErrorKind::DeserializeError to specialize ErrorKind::Message (extract::path::ErrorKind) (#2720)

This commit introduces another `extract::path::ErrorKind` variant that captures the
serde error nominally captured through the `serde:🇩🇪: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.

Co-authored-by: David Mládek <david.mladek.cz@gmail.com>
This commit is contained in:
Vegard Sandengen 2024-11-11 16:56:25 +00:00 committed by GitHub
parent 6e0559e687
commit 59a2960e42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 114 additions and 6 deletions

View file

@ -94,7 +94,7 @@ mod tests {
let res = client.get("/NaN").await;
assert_eq!(
res.text().await,
"Invalid URL: Cannot parse `\"NaN\"` to a `u32`"
"Invalid URL: Cannot parse `NaN` to a `u32`"
);
}
}

View file

@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
This allows middleware to add bodies to requests without needing to manually set `content-length` ([#2897])
- **breaking:** Remove `WebSocket::close`.
Users should explicitly send close messages themselves. ([#2974])
- **added:** Extend `FailedToDeserializePathParams::kind` enum with (`ErrorKind::DeserializeError`)
This new variant captures both `key`, `value`, and `message` from named path parameters parse errors,
instead of only deserialization error message in `ErrorKind::Message`. ([#2720])
[#2897]: https://github.com/tokio-rs/axum/pull/2897
[#2903]: https://github.com/tokio-rs/axum/pull/2903
@ -28,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#2974]: https://github.com/tokio-rs/axum/pull/2974
[#2978]: https://github.com/tokio-rs/axum/pull/2978
[#2992]: https://github.com/tokio-rs/axum/pull/2992
[#2720]: https://github.com/tokio-rs/axum/pull/2720
# 0.8.0

View file

@ -94,7 +94,21 @@ 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: PathDeserializationError| {
if let ErrorKind::Message(message) = &e.kind {
PathDeserializationError::new(ErrorKind::DeserializeError {
key: key.to_string(),
value: value.as_str().to_owned(),
message: message.to_owned(),
})
} else {
e
}
})
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -362,7 +376,19 @@ 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: PathDeserializationError| {
if let (ErrorKind::Message(message), Some(key)) = (&e.kind, self.key.as_ref()) {
PathDeserializationError::new(ErrorKind::DeserializeError {
key: key.key().to_owned(),
value: self.value.as_str().to_owned(),
message: message.to_owned(),
})
} else {
e
}
})
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -608,6 +634,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::*;
@ -926,4 +961,17 @@ mod tests {
}
);
}
#[test]
fn test_deserialize_key_value() {
test_parse_error!(
vec![("id", "123123-123-123123")],
uuid::Uuid,
ErrorKind::DeserializeError {
key: "id".to_owned(),
value: "123123-123-123123".to_owned(),
message: "UUID parsing failed: invalid group count: expected 5, found 3".to_owned(),
}
);
}
}

View file

@ -303,6 +303,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),
}
@ -331,20 +341,25 @@ impl fmt::Display for ErrorKind {
expected_type,
} => write!(
f,
"Cannot parse `{key}` with value `{value:?}` to a `{expected_type}`"
"Cannot parse `{key}` with value `{value}` to a `{expected_type}`"
),
ErrorKind::ParseError {
value,
expected_type,
} => write!(f, "Cannot parse `{value:?}` to a `{expected_type}`"),
} => write!(f, "Cannot parse `{value}` to a `{expected_type}`"),
ErrorKind::ParseErrorAtIndex {
index,
value,
expected_type,
} => write!(
f,
"Cannot parse value at index {index} with value `{value:?}` to a `{expected_type}`"
"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}"),
}
}
}
@ -369,6 +384,7 @@ impl FailedToDeserializePathParams {
pub fn body_text(&self) -> String {
match self.0.kind {
ErrorKind::Message(_)
| ErrorKind::DeserializeError { .. }
| ErrorKind::InvalidUtf8InPathParam { .. }
| ErrorKind::ParseError { .. }
| ErrorKind::ParseErrorAtIndex { .. }
@ -383,6 +399,7 @@ impl FailedToDeserializePathParams {
pub fn status(&self) -> StatusCode {
match self.0.kind {
ErrorKind::Message(_)
| ErrorKind::DeserializeError { .. }
| ErrorKind::InvalidUtf8InPathParam { .. }
| ErrorKind::ParseError { .. }
| ErrorKind::ParseErrorAtIndex { .. }
@ -917,4 +934,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<uuid::Uuid>| 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"#
);
}
}