Skip to content

Commit ca68ec5

Browse files
LarryOstermanCopilotheaths
authored
Reworked error handling to use new check_status function (#2982)
Fixed layering violation because `HttpError` was defined in typespec_client_core but was an azure_core construct. Simplified service client error management. --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Heath Stewart <[email protected]>
1 parent 96e92ce commit ca68ec5

File tree

33 files changed

+568
-1674
lines changed

33 files changed

+568
-1674
lines changed

sdk/core/azure_core/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- Added `TryFrom<T> for RequestContent<T, JsonFormat>` for JSON primitives.
1010
- Added support for WASM to the `async_runtime` module.
1111
- Added logging policy to log HTTP requests and responses in the pipeline. As a part of this change, sanitization support was added to places which log HTTP headers and URLs. The `azure_core::http::ClientOptions` has been enhanced with a `LoggingOptions` which allows a user/service client to specify headers or URL query parameters which should be allowed. Note that the sanitization feature is disabled if you build with the `debug` feature enabled.
12+
- Added support for a new `azure_core::error::http::ErrorResponse` structure which describes an error according to the Azure REST API guidelines.
13+
- Added a new `azure_core::http::check_success(BufResponse)` function to convert a buffered response to an `ErrorKind::HttpResponse`.
1214

1315
### Breaking Changes
1416

sdk/core/azure_core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
184184
When a service call fails, the returned `Result` will contain an `Error`. The `Error` type provides a status property with an HTTP status code and an error_code property with a service-specific error code.
185185

186186
```rust no_run
187-
use azure_core::{error::{ErrorKind, HttpError}, http::{Response, StatusCode}};
187+
use azure_core::{error::ErrorKind, http::{Response, StatusCode}};
188188
use azure_identity::DeveloperToolsCredential;
189189
use azure_security_keyvault_secrets::SecretClient;
190190

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// cspell:ignore innererror
5+
6+
use crate::{
7+
error::{Error, ErrorKind},
8+
http::{headers::ERROR_CODE, BufResponse},
9+
};
10+
use serde::Deserialize;
11+
use std::{collections::HashMap, str};
12+
13+
/// An HTTP error response.
14+
///
15+
/// Implements a standard "ErrorResponse" as described in the [API guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors).
16+
///
17+
/// Can be converted from an `[Error]` if it is of kind `[ErrorKind::HttpResponse]` and has a raw response.
18+
///
19+
/// # Example
20+
///
21+
/// Converting an `Error` to an `ErrorResponse`:
22+
///
23+
///``` no_run
24+
/// use azure_core::error::ErrorResponse;
25+
/// # let err = azure_core::Error::from(azure_core::error::ErrorKind::DataConversion);
26+
/// let error_response = ErrorResponse::try_from(err).expect("expected an ErrorResponse");
27+
///```
28+
///
29+
///
30+
#[derive(Debug, Deserialize)]
31+
#[serde(rename_all = "camelCase")]
32+
pub struct ErrorResponse {
33+
/// The error details.
34+
pub error: Option<ErrorDetail>,
35+
}
36+
37+
impl TryFrom<Error> for ErrorResponse {
38+
type Error = Error;
39+
40+
fn try_from(value: Error) -> Result<Self, Self::Error> {
41+
match value.kind() {
42+
ErrorKind::HttpResponse { raw_response, .. } => {
43+
let error_response: Option<crate::Result<ErrorResponse>> = raw_response
44+
.as_ref()
45+
.map(|raw| serde_json::from_slice(raw.body()).map_err(Error::from));
46+
match error_response {
47+
Some(result) => Ok(result?),
48+
None => Err(value),
49+
}
50+
}
51+
_ => Err(value),
52+
}
53+
}
54+
}
55+
56+
/// Details about an error returned from a service.
57+
///
58+
/// Implements a standard "ErrorDetails" as described in the [API guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors).
59+
#[derive(Debug, Deserialize)]
60+
#[serde(rename_all = "camelCase")]
61+
pub struct ErrorDetail {
62+
/// The error code. A machine readable error code defined by the service.
63+
pub code: Option<String>,
64+
65+
/// A human-readable error message describing the error.
66+
pub message: Option<String>,
67+
68+
/// The target of the error (for example, the name of the property in error).
69+
pub target: Option<String>,
70+
71+
/// Additional details about the error.
72+
#[serde(default)]
73+
pub details: Vec<ErrorDetail>,
74+
75+
/// An inner error that may have more specific information about the root cause of the error.
76+
#[serde(rename = "innererror")]
77+
pub inner_error: Option<InnerError>,
78+
79+
/// Additional properties that may be returned with the error.
80+
#[serde(flatten)]
81+
pub additional_properties: HashMap<String, serde_json::Value>,
82+
}
83+
84+
/// Inner error information about an error returned from a service.
85+
///
86+
/// Implements a standard "InnerError" as described in the [API guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors).
87+
#[derive(Debug, Deserialize)]
88+
#[serde(rename_all = "camelCase")]
89+
pub struct InnerError {
90+
/// A more specific error than was contained in the containing error.
91+
pub code: Option<String>,
92+
93+
/// An object containing more specific information than the current object about the error.
94+
#[serde(rename = "innererror")]
95+
pub inner_error: Option<Box<InnerError>>,
96+
}
97+
98+
/// Internal struct to help with deserialization without allocating Strings.
99+
#[derive(Debug, Deserialize)]
100+
struct ErrorResponseInternal<'a> {
101+
#[serde(borrow)]
102+
error: ErrorDetailsInternal<'a>,
103+
}
104+
105+
#[derive(Debug, Deserialize)]
106+
struct ErrorDetailsInternal<'a> {
107+
code: Option<&'a str>,
108+
message: Option<&'a str>,
109+
}
110+
111+
/// Checks if the response is a success and if not, creates an appropriate error.
112+
///
113+
/// # Arguments
114+
/// * `response` - The HTTP response to check.
115+
///
116+
/// # Returns
117+
/// * `Ok(RawResponse)` if the response is a success.
118+
/// * `Err(Error)` if the response is an error, with details extracted from the response
119+
/// body if possible.
120+
///
121+
pub async fn check_success(response: BufResponse) -> crate::Result<BufResponse> {
122+
let status = response.status();
123+
if status.is_success() {
124+
return Ok(response);
125+
}
126+
127+
let response = response.try_into_raw_response().await?;
128+
129+
// If there's no body, we can't extract any more information.
130+
if response.body().is_empty() {
131+
let code = response.headers().get_optional_str(&ERROR_CODE);
132+
let error_kind = ErrorKind::http_response(status, code.map(str::to_owned));
133+
return Err(Error::message(error_kind, status.to_string()));
134+
}
135+
let internal_response =
136+
serde_json::de::from_slice::<ErrorResponseInternal>(response.body()).map_err(Error::from);
137+
138+
let internal_response = match internal_response {
139+
Ok(r) => r,
140+
Err(_) => {
141+
// If we can't parse the body, return a generic error with the status code and body
142+
let code = response.headers().get_optional_str(&ERROR_CODE);
143+
let error_kind = ErrorKind::http_response(
144+
status,
145+
Some(code.map_or_else(|| response.status().to_string(), str::to_owned)),
146+
);
147+
return Err(Error::message(
148+
error_kind,
149+
format!(
150+
"{}: {}",
151+
status,
152+
str::from_utf8(response.body()).unwrap_or("(invalid utf-8 in body)")
153+
),
154+
));
155+
}
156+
};
157+
158+
// We give priority to the error code in the header, and try the body version if it's not present.
159+
let code = response.headers().get_optional_str(&ERROR_CODE);
160+
let code = code.or(internal_response.error.code);
161+
162+
let error_kind = ErrorKind::http_response(status, code.map(str::to_owned));
163+
164+
Err(Error::message(
165+
error_kind,
166+
internal_response
167+
.error
168+
.message
169+
.map_or_else(|| status.to_string(), |m| m.to_owned()),
170+
))
171+
}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use super::*;
176+
use crate::http::{headers, headers::Headers, StatusCode};
177+
use crate::Bytes;
178+
179+
#[tokio::test]
180+
async fn matching_against_http_error() {
181+
let mut headers = Headers::new();
182+
headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
183+
let response = BufResponse::from_bytes(
184+
StatusCode::ImATeapot,
185+
headers,
186+
Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
187+
);
188+
189+
let err = check_success(response).await.unwrap_err();
190+
let kind = err.kind();
191+
assert!(matches!(
192+
kind,
193+
ErrorKind::HttpResponse {
194+
status: StatusCode::ImATeapot,
195+
error_code,
196+
raw_response: None
197+
}
198+
if error_code.as_deref() == Some("teapot")
199+
));
200+
}
201+
202+
#[tokio::test]
203+
async fn matching_against_http_error_no_body() {
204+
let mut headers = Headers::new();
205+
headers.insert(headers::ERROR_CODE, "testError".to_string());
206+
let response = BufResponse::from_bytes(StatusCode::ImATeapot, headers, Bytes::new());
207+
208+
let err = check_success(response).await.unwrap_err();
209+
let kind = err.kind();
210+
assert_eq!(
211+
*kind,
212+
ErrorKind::HttpResponse {
213+
status: StatusCode::ImATeapot,
214+
error_code: Some("testError".to_string()),
215+
raw_response: None
216+
}
217+
);
218+
}
219+
220+
#[tokio::test]
221+
async fn matching_against_http_error_invalid_body() {
222+
let mut headers = Headers::new();
223+
headers.insert(headers::ERROR_CODE, "testError".to_string());
224+
let response = BufResponse::from_bytes(
225+
StatusCode::ImATeapot,
226+
headers,
227+
Bytes::from_static(br#"{"json": "error"}"#),
228+
);
229+
230+
let err = check_success(response).await.unwrap_err();
231+
let kind = err.kind();
232+
assert_eq!(
233+
*kind,
234+
ErrorKind::HttpResponse {
235+
status: StatusCode::ImATeapot,
236+
error_code: Some("testError".to_string()),
237+
raw_response: None
238+
}
239+
);
240+
assert!(err.to_string().contains(r#"{"json": "error"}"#));
241+
}
242+
243+
#[test]
244+
fn deserialize_to_error_response() {
245+
let err : ErrorResponse = serde_json::from_slice (br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#)
246+
.expect("Parse success.");
247+
err.error.as_ref().expect("error should be set");
248+
249+
println!("{:?}", &err);
250+
assert_eq!(
251+
err.error.as_ref().unwrap().code,
252+
Some("InvalidRequest".to_string())
253+
);
254+
assert_eq!(
255+
err.error.as_ref().unwrap().message,
256+
Some("The request object is not recognized.".to_string())
257+
);
258+
assert!(err.error.as_ref().unwrap().inner_error.is_some());
259+
assert_eq!(
260+
err.error
261+
.as_ref()
262+
.unwrap()
263+
.inner_error
264+
.as_ref()
265+
.unwrap()
266+
.code,
267+
Some("InvalidKey".to_string())
268+
);
269+
assert!(err
270+
.error
271+
.as_ref()
272+
.unwrap()
273+
.additional_properties
274+
.contains_key("key"));
275+
}
276+
277+
#[tokio::test]
278+
async fn convert_error_to_error_response() -> crate::Result<()> {
279+
{
280+
let err: Error = Error::from(ErrorKind::HttpResponse {
281+
status: StatusCode::BadRequest,
282+
error_code: Some("testError".to_string()),
283+
raw_response: None,
284+
});
285+
let _error_response = ErrorResponse::try_from(err)
286+
.expect_err("expected an error because there is no raw_response");
287+
}
288+
{
289+
let buf_response = BufResponse::from_bytes(
290+
StatusCode::BadRequest,
291+
Headers::new(),
292+
Bytes::from_static(br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#),
293+
);
294+
let err: Error = Error::from(ErrorKind::HttpResponse {
295+
status: StatusCode::BadRequest,
296+
error_code: Some("testError".to_string()),
297+
raw_response: Some(Box::new(buf_response.try_into_raw_response().await?)),
298+
});
299+
let error_response = ErrorResponse::try_from(err).expect("expected an ErrorResponse");
300+
error_response.error.as_ref().expect("error should be set");
301+
println!("{:?}", &error_response);
302+
assert_eq!(
303+
error_response.error.as_ref().unwrap().code,
304+
Some("InvalidRequest".to_string())
305+
);
306+
}
307+
Ok(())
308+
}
309+
310+
#[tokio::test]
311+
async fn convert_buf_response_to_error_response() -> crate::Result<()> {
312+
{
313+
let buf_response = BufResponse::from_bytes(
314+
StatusCode::BadRequest,
315+
Headers::new(),
316+
Bytes::from_static(br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#),
317+
);
318+
let error_response: ErrorResponse = buf_response
319+
.into_body()
320+
.json()
321+
.await
322+
.expect("expected an ErrorResponse");
323+
error_response.error.as_ref().expect("error should be set");
324+
println!("{:?}", &error_response);
325+
assert_eq!(
326+
error_response.error.as_ref().unwrap().code,
327+
Some("InvalidRequest".to_string())
328+
);
329+
}
330+
Ok(())
331+
}
332+
333+
#[tokio::test]
334+
async fn deserialize_to_error_response_internal() {
335+
let err :ErrorResponseInternal = serde_json::from_slice (br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey","key":"foo"}}}"#)
336+
.expect("Parse success.");
337+
println!("{:?}", &err);
338+
339+
assert_eq!(err.error.code, Some("InvalidRequest"));
340+
assert_eq!(
341+
err.error.message,
342+
Some("The request object is not recognized.")
343+
);
344+
}
345+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
pub use typespec_client_core::error::*;
5+
mod error_response;
6+
7+
pub use error_response::*;

sdk/core/azure_core/src/http/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ pub use typespec_client_core::http::{
2626
Method, NoFormat, StatusCode, Url,
2727
};
2828

29+
pub use crate::error::check_success;
2930
#[cfg(feature = "xml")]
3031
pub use typespec_client_core::http::XmlFormat;

sdk/core/azure_core/src/http/pipeline.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use super::policies::ClientRequestIdPolicy;
55
use crate::http::{
6-
headers::{ERROR_CODE, RETRY_AFTER_MS, X_MS_RETRY_AFTER_MS},
6+
headers::{RETRY_AFTER_MS, X_MS_RETRY_AFTER_MS},
77
policies::{
88
Policy, PublicApiInstrumentationPolicy, RequestInstrumentationPolicy, UserAgentPolicy,
99
},
@@ -106,7 +106,6 @@ impl Pipeline {
106106
let pipeline_options = pipeline_options.unwrap_or_else(|| PipelineOptions {
107107
retry_headers: RetryHeaders {
108108
retry_headers: vec![X_MS_RETRY_AFTER_MS, RETRY_AFTER_MS, RETRY_AFTER],
109-
error_header: Some(ERROR_CODE),
110109
},
111110
});
112111

0 commit comments

Comments
 (0)