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 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}