mirror of https://github.com/tokio-rs/axum
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:
parent
6e0559e687
commit
59a2960e42
|
@ -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`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue