tencent_sdk/core/
error.rs

1use http::{Method, StatusCode};
2use thiserror::Error;
3use url::{ParseError, Url};
4
5use crate::signing::SigningError;
6use std::fmt;
7
8pub type TencentCloudResult<T> = Result<T, TencentCloudError>;
9
10#[derive(Debug)]
11pub struct HttpFailure {
12    pub code: StatusCode,
13    pub method: Method,
14    pub url: Url,
15    pub body: String,
16}
17
18#[derive(Debug)]
19pub struct TransportFailure {
20    pub source: reqwest::Error,
21    pub method: Method,
22    pub url: Url,
23}
24
25#[derive(Debug)]
26pub struct ServiceFailure {
27    pub code: String,
28    pub message: String,
29    pub request_id: Option<String>,
30    pub kind: ServiceErrorKind,
31}
32
33#[derive(Debug, Clone, Copy, Eq, PartialEq)]
34pub enum ServiceErrorKind {
35    Auth,
36    Throttled,
37    Forbidden,
38    NotFound,
39    Validation,
40    Internal,
41    Unknown,
42}
43
44impl fmt::Display for ServiceErrorKind {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            ServiceErrorKind::Auth => write!(f, "auth"),
48            ServiceErrorKind::Throttled => write!(f, "throttled"),
49            ServiceErrorKind::Forbidden => write!(f, "forbidden"),
50            ServiceErrorKind::NotFound => write!(f, "not_found"),
51            ServiceErrorKind::Validation => write!(f, "validation"),
52            ServiceErrorKind::Internal => write!(f, "internal"),
53            ServiceErrorKind::Unknown => write!(f, "unknown"),
54        }
55    }
56}
57
58impl ServiceErrorKind {
59    /// Returns true if the error usually merits retrying the request.
60    pub fn is_retryable(self) -> bool {
61        matches!(
62            self,
63            ServiceErrorKind::Throttled | ServiceErrorKind::Internal | ServiceErrorKind::Unknown
64        )
65    }
66}
67
68impl ServiceFailure {
69    pub fn kind(&self) -> ServiceErrorKind {
70        self.kind
71    }
72}
73
74#[derive(Debug, Error)]
75#[non_exhaustive]
76pub enum TencentCloudError {
77    #[error("HTTP {code} ({method} {url}): {body}", code = context.code, method = context.method, url = context.url, body = context.body)]
78    Http { context: Box<HttpFailure> },
79
80    #[error("Transport error during {method} {url}: {source}", method = context.method, url = context.url, source = context.source)]
81    Transport { context: Box<TransportFailure> },
82
83    #[error("Transport build error: {source}")]
84    TransportBuild {
85        #[source]
86        source: reqwest::Error,
87    },
88
89    #[error(
90        "Tencent Cloud service error ({kind}) {code}: {message}{request_id}",
91        kind = context.kind,
92        code = context.code,
93        message = context.message,
94        request_id = DisplayRequestId(&context.request_id)
95    )]
96    Service { context: Box<ServiceFailure> },
97
98    #[error(transparent)]
99    Url(#[from] ParseError),
100
101    #[error(transparent)]
102    Json(#[from] serde_json::Error),
103
104    #[error(transparent)]
105    Signing(#[from] SigningError),
106}
107
108impl TencentCloudError {
109    pub fn http(code: StatusCode, method: Method, url: Url, body: String) -> Self {
110        Self::Http {
111            context: Box::new(HttpFailure {
112                code,
113                method,
114                url,
115                body,
116            }),
117        }
118    }
119
120    pub fn transport(source: reqwest::Error, method: Method, url: Url) -> Self {
121        Self::Transport {
122            context: Box::new(TransportFailure {
123                source,
124                method,
125                url,
126            }),
127        }
128    }
129
130    pub fn transport_build(source: reqwest::Error) -> Self {
131        Self::TransportBuild { source }
132    }
133
134    pub fn service(
135        code: impl Into<String>,
136        message: impl Into<String>,
137        request_id: Option<String>,
138    ) -> Self {
139        let code_owned = code.into();
140        Self::Service {
141            context: Box::new(ServiceFailure {
142                kind: classify_service_error(&code_owned),
143                code: code_owned,
144                message: message.into(),
145                request_id,
146            }),
147        }
148    }
149}
150
151struct DisplayRequestId<'a>(&'a Option<String>);
152
153impl fmt::Display for DisplayRequestId<'_> {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        if let Some(value) = self.0 {
156            write!(f, " (request {value})")
157        } else {
158            Ok(())
159        }
160    }
161}
162
163fn classify_service_error(code: &str) -> ServiceErrorKind {
164    if code.starts_with("AuthFailure")
165        || code.starts_with("InvalidCredential")
166        || code.starts_with("UnauthorizedOperation")
167    {
168        ServiceErrorKind::Auth
169    } else if code.starts_with("LimitExceeded")
170        || code.starts_with("RequestLimitExceeded")
171        || code.starts_with("Throttling")
172    {
173        ServiceErrorKind::Throttled
174    } else if code.starts_with("OperationDenied") || code.starts_with("Forbidden") {
175        ServiceErrorKind::Forbidden
176    } else if code.starts_with("ResourceNotFound") {
177        ServiceErrorKind::NotFound
178    } else if code.starts_with("InvalidParameter") || code.starts_with("MissingParameter") {
179        ServiceErrorKind::Validation
180    } else if code.starts_with("InternalError") {
181        ServiceErrorKind::Internal
182    } else {
183        ServiceErrorKind::Unknown
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn classifies_auth_errors() {
193        let err = TencentCloudError::service("AuthFailure.SecretIdNotFound", "missing id", None);
194        match err {
195            TencentCloudError::Service { context } => {
196                assert_eq!(context.kind(), ServiceErrorKind::Auth);
197                assert_eq!(context.code, "AuthFailure.SecretIdNotFound");
198                assert!(!context.kind().is_retryable());
199            }
200            _ => panic!("expected service error"),
201        }
202    }
203
204    #[test]
205    fn classifies_throttled_errors() {
206        let err = TencentCloudError::service(
207            "RequestLimitExceeded",
208            "too many calls",
209            Some("req".into()),
210        );
211        match err {
212            TencentCloudError::Service { context } => {
213                assert_eq!(context.kind(), ServiceErrorKind::Throttled);
214                assert!(context.kind().is_retryable());
215                assert_eq!(context.request_id.as_deref(), Some("req"));
216            }
217            _ => panic!("expected service error"),
218        }
219    }
220
221    #[test]
222    fn classifies_unknown_errors() {
223        let err = TencentCloudError::service("FooBar", "???", None);
224        match err {
225            TencentCloudError::Service { context } => {
226                assert_eq!(context.kind(), ServiceErrorKind::Unknown);
227                assert!(context.kind().is_retryable());
228            }
229            _ => panic!("expected service error"),
230        }
231    }
232}