Skip to main content

azure_core/error/
error_response.rs

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4// cspell:ignore innererror
5
6use crate::{
7    error::{Error, ErrorKind},
8    http::{headers::ERROR_CODE, AsyncRawResponse, RawResponse, StatusCode},
9};
10use serde::{Deserialize, Serialize};
11use std::{collections::HashMap, future::Future, 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(Clone, Debug, Deserialize, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ErrorResponse {
33    /// The error details.
34    pub error: Option<ErrorDetail>,
35}
36
37impl 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().as_ref()).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(Clone, Debug, Deserialize, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub 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, crate::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(Clone, Debug, Deserialize, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub 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)]
100struct ErrorResponseInternal<'a> {
101    #[serde(borrow)]
102    error: ErrorDetailsInternal<'a>,
103}
104
105#[derive(Debug, Deserialize)]
106struct ErrorDetailsInternal<'a> {
107    code: Option<&'a str>,
108    message: Option<&'a str>,
109}
110
111/// Represents a response from which we can get a [`StatusCode`] and collect into a [`RawResponse`].
112///
113/// This is intended for internal use only and implemented only by [`AsyncRawResponse`] and [`RawResponse`].
114pub trait Response: crate::private::Sealed {
115    /// Get the [`StatusCode`] from the response.
116    fn status(&self) -> StatusCode;
117
118    /// Collect into a [`RawResponse`].
119    fn try_into_raw_response(self) -> impl Future<Output = crate::Result<RawResponse>>;
120}
121
122impl crate::private::Sealed for AsyncRawResponse {}
123impl crate::private::Sealed for RawResponse {}
124
125impl Response for AsyncRawResponse {
126    fn status(&self) -> StatusCode {
127        self.status()
128    }
129
130    fn try_into_raw_response(self) -> impl Future<Output = crate::Result<RawResponse>> {
131        self.try_into_raw_response()
132    }
133}
134
135impl Response for RawResponse {
136    fn status(&self) -> StatusCode {
137        self.status()
138    }
139
140    #[inline]
141    fn try_into_raw_response(self) -> impl Future<Output = crate::Result<RawResponse>> {
142        std::future::ready(Ok(self))
143    }
144}
145
146/// Options for customizing the behavior of `check_success`.
147#[derive(Debug, Default)]
148pub struct CheckSuccessOptions {
149    /// A list of HTTP status codes that should be considered successful.
150    ///
151    /// If this list is empty, any 2xx status code is considered successful.
152    pub success_codes: &'static [u16],
153}
154
155/// Checks if the response is a success and if not, creates an appropriate error.
156///
157/// # Arguments
158/// * `response` - The HTTP response to check.
159/// * `options` - Optional parameters to customize the success criteria.
160///
161/// # Returns
162/// * `Ok(RawResponse)` if the response is a success.
163/// * `Err(Error)` if the response is an error, with details extracted from the response
164///   body if possible.
165///
166pub async fn check_success<T: Response>(
167    response: T,
168    options: Option<CheckSuccessOptions>,
169) -> crate::Result<T> {
170    let status = response.status();
171
172    if options
173        .as_ref()
174        .map(|o| {
175            if o.success_codes.is_empty() {
176                status.is_success()
177            } else {
178                o.success_codes.contains(&status)
179            }
180        })
181        .unwrap_or_else(|| status.is_success())
182    {
183        return Ok(response);
184    }
185
186    let raw_response = response.try_into_raw_response().await?;
187
188    // If there's no body, we can't extract any more information.
189    if raw_response.body().is_empty() {
190        let error_code = raw_response
191            .headers()
192            .get_optional_str(&ERROR_CODE)
193            .map(str::to_owned);
194        let error_kind = ErrorKind::HttpResponse {
195            status,
196            error_code,
197            raw_response: Some(Box::new(raw_response)),
198        };
199        return Err(Error::with_message(error_kind, status.to_string()));
200    }
201    let internal_response =
202        serde_json::de::from_slice::<ErrorResponseInternal>(raw_response.body())
203            .map_err(Error::from);
204
205    let internal_response = match internal_response {
206        Ok(r) => r,
207        Err(_) => {
208            // If we can't parse the body, return a generic error with the status code and body
209            let error_code = raw_response
210                .headers()
211                .get_optional_str(&ERROR_CODE)
212                .map_or_else(|| raw_response.status().to_string(), str::to_owned);
213            let message = str::from_utf8(raw_response.body())
214                .unwrap_or("(invalid utf-8 in body)")
215                .to_string();
216            let error_kind = ErrorKind::HttpResponse {
217                status,
218                error_code: Some(error_code),
219                raw_response: Some(Box::new(raw_response)),
220            };
221            return Err(Error::with_message(
222                error_kind,
223                format!("{}: {}", status, message),
224            ));
225        }
226    };
227
228    // We give priority to the error code in the header, and try the body version if it's not present.
229    let error_code = raw_response
230        .headers()
231        .get_optional_str(&ERROR_CODE)
232        .or(internal_response.error.code)
233        .map(str::to_owned);
234    let message = internal_response
235        .error
236        .message
237        .map_or_else(|| status.to_string(), str::to_owned);
238    let error_kind = ErrorKind::HttpResponse {
239        status,
240        error_code,
241        raw_response: Some(Box::new(raw_response)),
242    };
243
244    Err(Error::with_message(error_kind, message))
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::http::{headers, headers::Headers, StatusCode};
251    use crate::Bytes;
252
253    #[tokio::test]
254    async fn matching_against_http_error() {
255        let mut headers = Headers::new();
256        headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
257        let response = AsyncRawResponse::from_bytes(
258            StatusCode::ImATeapot,
259            headers,
260            Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
261        );
262
263        let err = check_success(response, None).await.unwrap_err();
264        let kind = err.kind();
265        assert!(matches!(
266            kind,
267            ErrorKind::HttpResponse {
268                status: StatusCode::ImATeapot,
269                error_code,
270                raw_response: Some(_),
271            }
272            if error_code.as_deref() == Some("teapot")
273        ));
274    }
275
276    #[tokio::test]
277    async fn matching_against_custom_http_error_empty_set() {
278        let mut headers = Headers::new();
279        headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
280        let response = AsyncRawResponse::from_bytes(
281            StatusCode::ImATeapot,
282            headers,
283            Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
284        );
285
286        let err = check_success(response, Some(CheckSuccessOptions { success_codes: &[] }))
287            .await
288            .unwrap_err();
289        let kind = err.kind();
290        assert!(matches!(
291            kind,
292            ErrorKind::HttpResponse {
293                status: StatusCode::ImATeapot,
294                error_code,
295                raw_response: Some(_),
296            }
297            if error_code.as_deref() == Some("teapot")
298        ));
299    }
300
301    #[tokio::test]
302    async fn matching_against_custom_http_error_in_set() {
303        let mut headers = Headers::new();
304        headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
305        let response = AsyncRawResponse::from_bytes(
306            StatusCode::ImATeapot,
307            headers,
308            Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
309        );
310
311        let _ = check_success(
312            response,
313            Some(CheckSuccessOptions {
314                success_codes: &[418],
315            }),
316        )
317        .await
318        .expect("Should be a success return");
319    }
320
321    #[tokio::test]
322    async fn matching_against_custom_http_error_in_set_success_should_fail() {
323        let mut headers = Headers::new();
324        headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
325        let response = AsyncRawResponse::from_bytes(
326            StatusCode::Ok,
327            headers,
328            Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
329        );
330
331        let err = check_success(
332            response,
333            Some(CheckSuccessOptions {
334                success_codes: &[418],
335            }),
336        )
337        .await
338        .expect_err("Should be a failure return");
339        let kind = err.kind();
340        assert!(matches!(
341            kind,
342            ErrorKind::HttpResponse {
343                status: StatusCode::Ok,
344                error_code,
345                raw_response: Some(_),
346            }
347            if error_code.as_deref() == Some("teapot")
348        ));
349    }
350
351    #[tokio::test]
352    async fn matching_against_http_error_no_body() {
353        let mut headers = Headers::new();
354        headers.insert(headers::ERROR_CODE, "testError".to_string());
355        let response = AsyncRawResponse::from_bytes(StatusCode::ImATeapot, headers, Bytes::new());
356
357        let err = check_success(response, None).await.unwrap_err();
358        let kind = err.kind();
359        assert!(matches!(
360            kind,
361            ErrorKind::HttpResponse {
362                status: StatusCode::ImATeapot,
363                error_code,
364                raw_response: Some(_),
365            }
366            if error_code.as_deref() == Some("testError")
367        ));
368    }
369
370    #[tokio::test]
371    async fn matching_against_http_error_invalid_body() {
372        let mut headers = Headers::new();
373        headers.insert(headers::ERROR_CODE, "testError".to_string());
374        let response = AsyncRawResponse::from_bytes(
375            StatusCode::ImATeapot,
376            headers,
377            Bytes::from_static(br#"{"json": "error"}"#),
378        );
379
380        let err = check_success(response, None).await.unwrap_err();
381        let ErrorKind::HttpResponse {
382            status,
383            error_code: Some(error_code),
384            raw_response: Some(raw_response),
385        } = err.kind()
386        else {
387            panic!("expected ErrorKind::HttpResponse");
388        };
389
390        assert!(err.to_string().contains(r#"{"json": "error"}"#));
391        assert_eq!(status, &StatusCode::ImATeapot);
392        assert_eq!(error_code, "testError");
393        assert_eq!(raw_response.status(), StatusCode::ImATeapot);
394        assert_eq!(raw_response.headers().iter().count(), 1);
395        assert!(
396            matches!(str::from_utf8(raw_response.body()), Ok(body) if body == r#"{"json": "error"}"#)
397        );
398    }
399
400    #[test]
401    fn deserialize_to_error_response() {
402        let err : ErrorResponse = serde_json::from_slice (br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#)
403            .expect("Parse success.");
404        err.error.as_ref().expect("error should be set");
405
406        println!("{:?}", err);
407        assert_eq!(
408            err.error.as_ref().unwrap().code,
409            Some("InvalidRequest".to_string())
410        );
411        assert_eq!(
412            err.error.as_ref().unwrap().message,
413            Some("The request object is not recognized.".to_string())
414        );
415        assert!(err.error.as_ref().unwrap().inner_error.is_some());
416        assert_eq!(
417            err.error
418                .as_ref()
419                .unwrap()
420                .inner_error
421                .as_ref()
422                .unwrap()
423                .code,
424            Some("InvalidKey".to_string())
425        );
426        assert!(err
427            .error
428            .as_ref()
429            .unwrap()
430            .additional_properties
431            .contains_key("key"));
432    }
433
434    #[test]
435    fn serialize_error_response() {
436        let error_response = ErrorResponse {
437            error: Some(ErrorDetail {
438                code: Some("InvalidRequest".to_string()),
439                message: Some("The request object is not recognized.".to_string()),
440                target: None,
441                details: vec![],
442                inner_error: Some(InnerError {
443                    code: Some("InvalidKey".to_string()),
444                    inner_error: None,
445                }),
446                additional_properties: HashMap::from([(
447                    "key".to_string(),
448                    crate::Value::from("foo"),
449                )]),
450            }),
451        };
452
453        let json = serde_json::to_value(&error_response).expect("Serialize success.");
454        let error_obj = json.get("error").expect("error should be set");
455        assert_eq!(
456            error_obj.get("code").and_then(|v| v.as_str()),
457            Some("InvalidRequest")
458        );
459        assert_eq!(
460            error_obj.get("message").and_then(|v| v.as_str()),
461            Some("The request object is not recognized.")
462        );
463        assert_eq!(
464            error_obj
465                .get("innererror")
466                .and_then(|v| v.get("code"))
467                .and_then(|v| v.as_str()),
468            Some("InvalidKey")
469        );
470        assert_eq!(error_obj.get("key").and_then(|v| v.as_str()), Some("foo"));
471        assert!(error_obj.get("target").is_some());
472        assert!(error_obj.get("details").is_some());
473    }
474
475    #[tokio::test]
476    async fn convert_error_to_error_response() -> crate::Result<()> {
477        {
478            let err: Error = Error::from(ErrorKind::HttpResponse {
479                status: StatusCode::BadRequest,
480                error_code: Some("testError".to_string()),
481                raw_response: None,
482            });
483            let _error_response = ErrorResponse::try_from(err)
484                .expect_err("expected an error because there is no raw_response");
485        }
486        {
487            let buf_response = AsyncRawResponse::from_bytes(
488                StatusCode::BadRequest,
489                Headers::new(),
490                Bytes::from_static(br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#),
491            );
492            let err: Error = Error::from(ErrorKind::HttpResponse {
493                status: StatusCode::BadRequest,
494                error_code: Some("testError".to_string()),
495                raw_response: Some(Box::new(buf_response.try_into_raw_response().await?)),
496            });
497            let error_response = ErrorResponse::try_from(err).expect("expected an ErrorResponse");
498            error_response.error.as_ref().expect("error should be set");
499            println!("{:?}", error_response);
500            assert_eq!(
501                error_response.error.as_ref().unwrap().code,
502                Some("InvalidRequest".to_string())
503            );
504        }
505        Ok(())
506    }
507
508    #[tokio::test]
509    async fn convert_buf_response_to_error_response() -> crate::Result<()> {
510        {
511            let buf_response = AsyncRawResponse::from_bytes(
512                StatusCode::BadRequest,
513                Headers::new(),
514                Bytes::from_static(br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#),
515            );
516            let error_response: ErrorResponse = buf_response
517                .try_into_raw_response()
518                .await?
519                .into_body()
520                .json()
521                .expect("expected an ErrorResponse");
522            error_response.error.as_ref().expect("error should be set");
523            println!("{:?}", error_response);
524            assert_eq!(
525                error_response.error.as_ref().unwrap().code,
526                Some("InvalidRequest".to_string())
527            );
528        }
529        Ok(())
530    }
531
532    #[test]
533    fn clone_error_response() {
534        let response: ErrorResponse = serde_json::from_slice(
535            br#"{"error":{"code":"InvalidRequest","message":"bad request","innererror":{"code":"InvalidKey"},"extra":"value"}}"#,
536        )
537        .expect("deserialize");
538        let cloned = response.clone();
539        let detail = cloned.error.as_ref().expect("error detail present");
540        assert_eq!(detail.code.as_deref(), Some("InvalidRequest"));
541        assert_eq!(detail.message.as_deref(), Some("bad request"));
542        assert_eq!(
543            detail
544                .inner_error
545                .as_ref()
546                .and_then(|ie| ie.code.as_deref()),
547            Some("InvalidKey"),
548        );
549        assert!(detail.additional_properties.contains_key("extra"));
550    }
551
552    #[tokio::test]
553    async fn deserialize_to_error_response_internal() {
554        let err :ErrorResponseInternal = serde_json::from_slice (br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey","key":"foo"}}}"#)
555            .expect("Parse success.");
556        println!("{:?}", err);
557
558        assert_eq!(err.error.code, Some("InvalidRequest"));
559        assert_eq!(
560            err.error.message,
561            Some("The request object is not recognized.")
562        );
563    }
564}