1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use std::time::Duration;

use crate::resource::{ErrorResponse, OAuth2ErrorResponse};
use reqwest::StatusCode;
use thiserror::Error;

/// An alias to `Result` of [`Error`][error].
///
/// [error]: ./struct.Error.html
pub type Result<T> = std::result::Result<T, Error>;

/// Error of API request
#[derive(Debug, Error)]
#[error(transparent)]
pub struct Error {
    inner: Box<ErrorKind>,
}

#[derive(Debug, Error)]
enum ErrorKind {
    // Errors about ser/de are included.
    #[error("Request error: {0}")]
    RequestError(#[source] reqwest::Error),
    #[error("Unexpected response: {reason}")]
    UnexpectedResponse { reason: &'static str },
    #[error("Api error with {status}: ({}) {}", .response.code, .response.message)]
    ErrorResponse {
        status: StatusCode,
        response: ErrorResponse,
        retry_after: Option<u32>,
    },
    #[error("OAuth2 error with {status}: ({}) {}", .response.error, .response.error_description)]
    OAuth2Error {
        status: StatusCode,
        response: OAuth2ErrorResponse,
        retry_after: Option<u32>,
    },
}

impl Error {
    pub(crate) fn from_error_response(
        status: StatusCode,
        response: ErrorResponse,
        retry_after: Option<u32>,
    ) -> Self {
        Self {
            inner: Box::new(ErrorKind::ErrorResponse {
                status,
                response,
                retry_after,
            }),
        }
    }

    pub(crate) fn unexpected_response(reason: &'static str) -> Self {
        Self {
            inner: Box::new(ErrorKind::UnexpectedResponse { reason }),
        }
    }

    pub(crate) fn from_oauth2_error_response(
        status: StatusCode,
        response: OAuth2ErrorResponse,
        retry_after: Option<u32>,
    ) -> Self {
        Self {
            inner: Box::new(ErrorKind::OAuth2Error {
                status,
                response,
                retry_after,
            }),
        }
    }

    /// Get the error response from API if caused by error status code.
    #[must_use]
    pub fn error_response(&self) -> Option<&ErrorResponse> {
        match &*self.inner {
            ErrorKind::ErrorResponse { response, .. } => Some(response),
            _ => None,
        }
    }

    /// Get the OAuth2 error response from API if caused by OAuth2 error response.
    #[must_use]
    pub fn oauth2_error_response(&self) -> Option<&OAuth2ErrorResponse> {
        match &*self.inner {
            ErrorKind::OAuth2Error { response, .. } => Some(response),
            _ => None,
        }
    }

    /// Get the HTTP status code if caused by error status code.
    #[must_use]
    pub fn status_code(&self) -> Option<StatusCode> {
        match &*self.inner {
            ErrorKind::RequestError(source) => source.status(),
            ErrorKind::UnexpectedResponse { .. } => None,
            ErrorKind::ErrorResponse { status, .. } | ErrorKind::OAuth2Error { status, .. } => {
                Some(*status)
            }
        }
    }

    /// Get the retry delay hint on rate limited (HTTP 429) or server unavailability, if any.
    ///
    /// This is parsed from response header `Retry-After`.
    /// See: <https://learn.microsoft.com/en-us/graph/throttling>
    #[must_use]
    pub fn retry_after(&self) -> Option<Duration> {
        match &*self.inner {
            ErrorKind::ErrorResponse { retry_after, .. }
            | ErrorKind::OAuth2Error { retry_after, .. } => {
                Some(Duration::from_secs((*retry_after)?.into()))
            }
            _ => None,
        }
    }
}

impl From<reqwest::Error> for Error {
    fn from(source: reqwest::Error) -> Self {
        Self {
            inner: Box::new(ErrorKind::RequestError(source)),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::error::Error as _;

    use super::*;

    #[test]
    fn error_source() {
        let err = reqwest::blocking::get("urn:urn").unwrap_err();
        let original_err_fmt = err.to_string();
        let source_err_fmt = Error::from(err).source().unwrap().to_string();
        assert_eq!(source_err_fmt, original_err_fmt);
    }
}