Skip to content

Commit 58fcc4e

Browse files
authored
[Proposal] Error Handling (#625)
* Add new error type * Use error type in ListDatabases * Use error type in ListDatabasesResponse * Add some docs * Treat Error type like wrapper when wrapping an error * Add helper macros * Make Error generic over operation specific errors: * Move CreateDatabase to the new error * Move pipeline to use new error type * Add way to downcast to error types directly * Add missing error type * PR feedback * Get mock framework working * Get generated services working * Introduce error code * Fix broken mock test * Format code * Seal the ResultExt trait
1 parent 6110666 commit 58fcc4e

29 files changed

+944
-169
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use super::{Error, ErrorKind};
2+
3+
impl From<crate::errors::Error> for Error {
4+
fn from(err: crate::errors::Error) -> Error {
5+
match err {
6+
crate::errors::Error::Json(e) => e.into(),
7+
crate::errors::Error::Header(e) => Error::new(ErrorKind::DataConversion, e),
8+
crate::errors::Error::Parse(e) => Error::new(ErrorKind::DataConversion, e),
9+
crate::errors::Error::HeadersNotFound(hs) => Error::with_message(
10+
ErrorKind::DataConversion,
11+
format!("headers not found: {}", hs.join(", ")),
12+
),
13+
crate::errors::Error::HeaderNotFound(h) => Error::with_message(
14+
ErrorKind::DataConversion,
15+
format!("header not found: {}", h),
16+
),
17+
crate::errors::Error::Http(e) => e.into(),
18+
crate::errors::Error::Stream(e) => Error::new(ErrorKind::Io, e),
19+
crate::errors::Error::GetToken(e) => Error::new(ErrorKind::Credential, e),
20+
crate::errors::Error::HttpPrepare(e) => e.into(),
21+
crate::errors::Error::Other(e) => Error::new(ErrorKind::Other, e),
22+
}
23+
}
24+
}
25+
26+
impl From<crate::errors::HttpError> for Error {
27+
fn from(err: crate::errors::HttpError) -> Error {
28+
match err {
29+
crate::HttpError::StatusCode { status, ref body } => Error::new(
30+
ErrorKind::http_response_from_body(status.as_u16(), body),
31+
err,
32+
),
33+
crate::HttpError::ExecuteRequest(e) => Error::new(ErrorKind::Io, e),
34+
crate::HttpError::ReadBytes(e) => Error::new(ErrorKind::Io, e),
35+
crate::HttpError::StreamReset(e) => Error::new(ErrorKind::Io, e),
36+
crate::HttpError::Utf8(e) => Error::new(ErrorKind::DataConversion, e),
37+
crate::HttpError::BuildResponse(e) => Error::new(ErrorKind::DataConversion, e),
38+
crate::HttpError::BuildClientRequest(e) => Error::new(ErrorKind::Other, e),
39+
}
40+
}
41+
}

sdk/core/src/error/http_error.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use crate::Response;
2+
3+
/// An unsuccessful HTTP response
4+
#[derive(Debug)]
5+
pub struct HttpError {
6+
status: u16,
7+
error_code: Option<String>,
8+
headers: std::collections::HashMap<String, String>,
9+
body: String,
10+
}
11+
12+
impl HttpError {
13+
/// Create an error from an http response.
14+
///
15+
/// This does not check whether the response was a success and should only be used with unsuccessul responses.
16+
pub async fn new(response: Response) -> Self {
17+
let status = response.status();
18+
let mut error_code = get_error_code_from_header(&response);
19+
let headers = response
20+
.headers()
21+
.into_iter()
22+
// TODO: the following will panic if a non-UTF8 header value is sent back
23+
// We should not panic but instead handle this gracefully
24+
.map(|(n, v)| (n.as_str().to_owned(), v.to_str().unwrap().to_owned()))
25+
.collect();
26+
let body = response.into_body_string().await;
27+
error_code = error_code.or_else(|| get_error_code_from_body(&body));
28+
HttpError {
29+
status: status.as_u16(),
30+
headers,
31+
error_code,
32+
body,
33+
}
34+
}
35+
36+
/// Get a reference to the http error's status.
37+
pub fn status(&self) -> u16 {
38+
self.status
39+
}
40+
41+
/// Get a reference to the http error's error code.
42+
pub fn error_code(&self) -> Option<&str> {
43+
self.error_code.as_deref()
44+
}
45+
}
46+
47+
impl std::fmt::Display for HttpError {
48+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49+
writeln!(f, "HttpError")?;
50+
writeln!(f, "\tStatus: {}", self.status)?;
51+
writeln!(
52+
f,
53+
"\tError Code: {}",
54+
self.error_code.as_deref().unwrap_or("unknown")
55+
)?;
56+
// TODO: sanitize body
57+
writeln!(f, "\tBody: \"{}\"", self.body)?;
58+
writeln!(f, "\tHeaders:")?;
59+
// TODO: sanitize headers
60+
for (k, v) in &self.headers {
61+
writeln!(f, "\t\t{}:{}", k, v)?;
62+
}
63+
64+
Ok(())
65+
}
66+
}
67+
68+
impl std::error::Error for HttpError {}
69+
70+
/// Gets the error code if it's present in the headers
71+
///
72+
/// For more info, see [here](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors)
73+
fn get_error_code_from_header(response: &Response) -> Option<String> {
74+
Some(
75+
response
76+
.headers()
77+
.get(http::header::HeaderName::from_static("x-ms-error-code"))?
78+
.to_str()
79+
.ok()?
80+
.to_owned(),
81+
)
82+
}
83+
84+
/// Gets the error code if it's present in the body
85+
///
86+
/// For more info, see [here](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors)
87+
pub(crate) fn get_error_code_from_body(body: &str) -> Option<String> {
88+
Some(
89+
serde_json::from_str::<serde_json::Value>(body)
90+
.ok()?
91+
.get("error")?
92+
.get("code")?
93+
.as_str()?
94+
.to_owned(),
95+
)
96+
}

sdk/core/src/error/hyperium_http.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use super::{Error, ErrorKind};
2+
3+
use http::header::{InvalidHeaderName, InvalidHeaderValue};
4+
use http::method::InvalidMethod;
5+
use http::status::InvalidStatusCode;
6+
use http::uri::{InvalidUri, InvalidUriParts};
7+
8+
impl From<http::Error> for super::Error {
9+
fn from(err: http::Error) -> super::Error {
10+
Error::new(ErrorKind::DataConversion, err)
11+
}
12+
}
13+
14+
impl From<InvalidHeaderName> for super::Error {
15+
fn from(err: InvalidHeaderName) -> super::Error {
16+
Error::new(ErrorKind::DataConversion, err)
17+
}
18+
}
19+
20+
impl From<InvalidHeaderValue> for super::Error {
21+
fn from(err: InvalidHeaderValue) -> super::Error {
22+
Error::new(ErrorKind::DataConversion, err)
23+
}
24+
}
25+
26+
impl From<InvalidMethod> for super::Error {
27+
fn from(err: InvalidMethod) -> super::Error {
28+
Error::new(ErrorKind::DataConversion, err)
29+
}
30+
}
31+
32+
impl From<InvalidStatusCode> for super::Error {
33+
fn from(err: InvalidStatusCode) -> super::Error {
34+
Error::new(ErrorKind::DataConversion, err)
35+
}
36+
}
37+
38+
impl From<InvalidUri> for super::Error {
39+
fn from(err: InvalidUri) -> super::Error {
40+
Error::new(ErrorKind::DataConversion, err)
41+
}
42+
}
43+
44+
impl From<InvalidUriParts> for super::Error {
45+
fn from(err: InvalidUriParts) -> super::Error {
46+
Error::new(ErrorKind::DataConversion, err)
47+
}
48+
}

sdk/core/src/error/macros.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/// A convenient way to create a new error using the normal formatting infrastructure
2+
#[macro_export]
3+
macro_rules! format_err {
4+
($kind:expr, $msg:literal $(,)?) => {{
5+
// Handle $:literal as a special case to make cargo-expanded code more
6+
// concise in the common case.
7+
$crate::error::Error::new($kind, $msg)
8+
}};
9+
($kind:expr, $msg:expr $(,)?) => {{
10+
$crate::error::Error::new($kind, $msg)
11+
}};
12+
($kind:expr, $msg:expr, $($arg:tt)*) => {{
13+
$crate::error::Error::new($kind, format!($msg, $($arg)*))
14+
}};
15+
}
16+
17+
/// Return early with an error if a condition is not satisfied.
18+
#[macro_export]
19+
macro_rules! ensure {
20+
($cond:expr, $kind:expr, $msg:literal $(,)?) => {
21+
if !$cond {
22+
return ::std::result::Result::Err($crate::format_err!($kind, $msg));
23+
}
24+
};
25+
($cond:expr, $kind:expr, dicate $msg:expr $(,)?) => {
26+
if !$cond {
27+
return ::std::result::Result::Err($crate::format_err!($kind, $msg));
28+
}
29+
};
30+
($cond:expr, $kind:expr, dicate $msg:expr, $($arg:tt)*) => {
31+
if !$cond {
32+
return ::std::result::Result::Err($crate::format_err!($kind, $msg, $($arg)*));
33+
}
34+
};
35+
}
36+
37+
/// Return early with an error if two expressions are not equal to each other.
38+
#[macro_export]
39+
macro_rules! ensure_eq {
40+
($left:expr, $right:expr, $kind:expr, $msg:literal $(,)?) => {
41+
$crate::ensure!($left == $right, $kind, $msg);
42+
};
43+
($left:expr, $right:expr, $kind:expr, dicate $msg:expr $(,)?) => {
44+
$crate::ensure!($left == $right, $kind, $msg);
45+
};
46+
($left:expr, $right:expr, $kind:expr, dicate $msg:expr, $($arg:tt)*) => {
47+
$crate::ensure!($left == $right, $kind, $msg, $($arg)*);
48+
};
49+
}
50+
51+
/// Return early with an error if two expressions are equal to each other.
52+
#[macro_export]
53+
macro_rules! ensure_ne {
54+
($left:expr, $right:expr, $kind:expr, $msg:literal $(,)?) => {
55+
$crate::ensure!($left != $right, $kind, $msg);
56+
};
57+
($left:expr, $right:expr, $kind:expr, dicate $msg:expr $(,)?) => {
58+
$crate::ensure!($left != $right, $kind, $msg);
59+
};
60+
($left:expr, $right:expr, $kind:expr, dicate $msg:expr, $($arg:tt)*) => {
61+
$crate::ensure!($left != $right, $kind, $msg, $($arg)*);
62+
};
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
use super::super::*;
68+
69+
#[derive(Debug, PartialEq, Copy, Clone)]
70+
struct OperationError;
71+
72+
impl Display for OperationError {
73+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74+
write!(f, "OperationError")
75+
}
76+
}
77+
78+
#[derive(thiserror::Error, Debug)]
79+
enum IntermediateError {
80+
#[error("second error")]
81+
Io(#[from] std::io::Error),
82+
}
83+
84+
#[test]
85+
fn ensure_works() {
86+
fn test_ensure(predicate: bool) -> Result<()> {
87+
ensure!(predicate, ErrorKind::Other, "predicate failed");
88+
Ok(())
89+
}
90+
91+
fn test_ensure_eq(item1: &str, item2: &str) -> Result<()> {
92+
ensure_eq!(item1, item2, ErrorKind::Other, "predicate failed");
93+
Ok(())
94+
}
95+
96+
fn test_ensure_ne(item1: &str, item2: &str) -> Result<()> {
97+
ensure_ne!(item1, item2, ErrorKind::Other, "predicate failed");
98+
Ok(())
99+
}
100+
101+
let err = test_ensure(false).unwrap_err();
102+
assert_eq!(format!("{}", err), "predicate failed");
103+
assert_eq!(err.kind(), &ErrorKind::Other);
104+
105+
assert!(test_ensure(true).is_ok());
106+
107+
let err = test_ensure_eq("foo", "bar").unwrap_err();
108+
assert_eq!(format!("{}", err), "predicate failed");
109+
assert_eq!(err.kind(), &ErrorKind::Other);
110+
111+
assert!(test_ensure_eq("foo", "foo").is_ok());
112+
113+
let err = test_ensure_ne("foo", "foo").unwrap_err();
114+
assert_eq!(format!("{}", err), "predicate failed");
115+
assert_eq!(err.kind(), &ErrorKind::Other);
116+
117+
assert!(test_ensure_ne("foo", "bar").is_ok());
118+
}
119+
}

0 commit comments

Comments
 (0)