1use http::{HeaderMap, Method, StatusCode};
2use std::{error::Error as StdError, fmt, time::Duration};
3
4pub type Result<T> = std::result::Result<T, Error>;
5
6pub(crate) type BoxError = Box<dyn StdError + Send + Sync + 'static>;
7
8#[derive(Debug, Clone, Copy, Eq, PartialEq)]
9#[non_exhaustive]
10pub enum ErrorKind {
11 InvalidConfig,
12 Transport,
13 Decode,
14 Auth,
15 NotFound,
16 Conflict,
17 RateLimited,
18 Api,
19}
20
21#[non_exhaustive]
22pub struct InvalidConfigError {
23 message: String,
24 base_url: Option<String>,
25 source: Option<BoxError>,
26}
27
28impl InvalidConfigError {
29 pub fn message(&self) -> &str {
30 &self.message
31 }
32
33 pub fn base_url(&self) -> Option<&str> {
34 self.base_url.as_deref()
35 }
36}
37
38impl fmt::Debug for InvalidConfigError {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 f.debug_struct("InvalidConfigError")
41 .field("message", &self.message)
42 .field("base_url", &self.base_url)
43 .field("has_source", &self.source.is_some())
44 .finish()
45 }
46}
47
48#[non_exhaustive]
49pub struct TransportError {
50 method: Method,
51 host: String,
52 path: String,
53 source: BoxError,
54}
55
56impl TransportError {
57 pub fn method(&self) -> &Method {
58 &self.method
59 }
60
61 pub fn host(&self) -> &str {
62 &self.host
63 }
64
65 pub fn path(&self) -> &str {
66 &self.path
67 }
68}
69
70impl fmt::Debug for TransportError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.debug_struct("TransportError")
73 .field("method", &self.method)
74 .field("host", &self.host)
75 .field("path", &self.path)
76 .field("has_source", &true)
77 .finish()
78 }
79}
80
81#[non_exhaustive]
82pub struct DecodeError {
83 status: Option<StatusCode>,
84 method: Method,
85 host: String,
86 path: String,
87 request_id: Option<String>,
88 body_snippet: Option<String>,
89 source: BoxError,
90}
91
92impl DecodeError {
93 pub fn status(&self) -> Option<StatusCode> {
94 self.status
95 }
96
97 pub fn method(&self) -> &Method {
98 &self.method
99 }
100
101 pub fn host(&self) -> &str {
102 &self.host
103 }
104
105 pub fn path(&self) -> &str {
106 &self.path
107 }
108
109 pub fn request_id(&self) -> Option<&str> {
110 self.request_id.as_deref()
111 }
112
113 pub fn body_snippet(&self) -> Option<&str> {
114 self.body_snippet.as_deref()
115 }
116}
117
118impl fmt::Debug for DecodeError {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 f.debug_struct("DecodeError")
121 .field("status", &self.status)
122 .field("method", &self.method)
123 .field("host", &self.host)
124 .field("path", &self.path)
125 .field("request_id", &self.request_id)
126 .field("body_snippet", &self.body_snippet)
127 .field("has_source", &true)
128 .finish()
129 }
130}
131
132#[non_exhaustive]
133pub struct ApiError {
134 status: Option<StatusCode>,
135 method: Option<Method>,
136 host: Option<String>,
137 path: Option<String>,
138 code: Option<String>,
139 message: Option<String>,
140 request_id: Option<String>,
141 body_snippet: Option<String>,
142}
143
144impl ApiError {
145 pub fn status(&self) -> Option<StatusCode> {
146 self.status
147 }
148
149 pub fn method(&self) -> Option<&Method> {
150 self.method.as_ref()
151 }
152
153 pub fn host(&self) -> Option<&str> {
154 self.host.as_deref()
155 }
156
157 pub fn path(&self) -> Option<&str> {
158 self.path.as_deref()
159 }
160
161 pub fn code(&self) -> Option<&str> {
162 self.code.as_deref()
163 }
164
165 pub fn message(&self) -> Option<&str> {
166 self.message.as_deref()
167 }
168
169 pub fn request_id(&self) -> Option<&str> {
170 self.request_id.as_deref()
171 }
172
173 pub fn body_snippet(&self) -> Option<&str> {
174 self.body_snippet.as_deref()
175 }
176}
177
178impl fmt::Debug for ApiError {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.debug_struct("ApiError")
181 .field("status", &self.status)
182 .field("method", &self.method)
183 .field("host", &self.host)
184 .field("path", &self.path)
185 .field("code", &self.code)
186 .field("message", &self.message)
187 .field("request_id", &self.request_id)
188 .field("body_snippet", &self.body_snippet)
189 .finish()
190 }
191}
192
193#[non_exhaustive]
194pub struct RateLimitedError {
195 api: ApiError,
196 retry_after: Option<Duration>,
197}
198
199impl RateLimitedError {
200 pub fn api(&self) -> &ApiError {
201 &self.api
202 }
203
204 pub fn retry_after(&self) -> Option<Duration> {
205 self.retry_after
206 }
207}
208
209impl fmt::Debug for RateLimitedError {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 f.debug_struct("RateLimitedError")
212 .field("api", &self.api)
213 .field("retry_after", &self.retry_after)
214 .finish()
215 }
216}
217
218#[derive(Debug)]
219#[non_exhaustive]
220pub enum Error {
221 InvalidConfig(Box<InvalidConfigError>),
222 Transport(Box<TransportError>),
223 Decode(Box<DecodeError>),
224 Auth(Box<ApiError>),
225 NotFound(Box<ApiError>),
226 Conflict(Box<ApiError>),
227 RateLimited(Box<RateLimitedError>),
228 Api(Box<ApiError>),
229}
230
231impl Error {
232 pub fn kind(&self) -> ErrorKind {
233 match self {
234 Error::InvalidConfig(_) => ErrorKind::InvalidConfig,
235 Error::Transport(_) => ErrorKind::Transport,
236 Error::Decode(_) => ErrorKind::Decode,
237 Error::Auth(_) => ErrorKind::Auth,
238 Error::NotFound(_) => ErrorKind::NotFound,
239 Error::Conflict(_) => ErrorKind::Conflict,
240 Error::RateLimited(_) => ErrorKind::RateLimited,
241 Error::Api(_) => ErrorKind::Api,
242 }
243 }
244
245 pub fn status(&self) -> Option<StatusCode> {
246 match self {
247 Error::InvalidConfig(_) => None,
248 Error::Transport(_) => None,
249 Error::Decode(err) => err.status,
250 Error::Auth(err) => err.status,
251 Error::NotFound(err) => err.status,
252 Error::Conflict(err) => err.status,
253 Error::RateLimited(err) => err.api.status,
254 Error::Api(err) => err.status,
255 }
256 }
257
258 pub fn method(&self) -> Option<&Method> {
259 match self {
260 Error::InvalidConfig(_) => None,
261 Error::Transport(err) => Some(&err.method),
262 Error::Decode(err) => Some(&err.method),
263 Error::Auth(err) => err.method.as_ref(),
264 Error::NotFound(err) => err.method.as_ref(),
265 Error::Conflict(err) => err.method.as_ref(),
266 Error::RateLimited(err) => err.api.method.as_ref(),
267 Error::Api(err) => err.method.as_ref(),
268 }
269 }
270
271 pub fn host(&self) -> Option<&str> {
272 match self {
273 Error::InvalidConfig(_) => None,
274 Error::Transport(err) => Some(&err.host),
275 Error::Decode(err) => Some(&err.host),
276 Error::Auth(err) => err.host.as_deref(),
277 Error::NotFound(err) => err.host.as_deref(),
278 Error::Conflict(err) => err.host.as_deref(),
279 Error::RateLimited(err) => err.api.host.as_deref(),
280 Error::Api(err) => err.host.as_deref(),
281 }
282 }
283
284 pub fn path(&self) -> Option<&str> {
285 match self {
286 Error::InvalidConfig(_) => None,
287 Error::Transport(err) => Some(&err.path),
288 Error::Decode(err) => Some(&err.path),
289 Error::Auth(err) => err.path.as_deref(),
290 Error::NotFound(err) => err.path.as_deref(),
291 Error::Conflict(err) => err.path.as_deref(),
292 Error::RateLimited(err) => err.api.path.as_deref(),
293 Error::Api(err) => err.path.as_deref(),
294 }
295 }
296
297 pub fn message(&self) -> Option<&str> {
298 match self {
299 Error::InvalidConfig(err) => Some(err.message.as_str()),
300 Error::Transport(_) => None,
301 Error::Decode(_) => None,
302 Error::Auth(err) => err.message.as_deref(),
303 Error::NotFound(err) => err.message.as_deref(),
304 Error::Conflict(err) => err.message.as_deref(),
305 Error::RateLimited(err) => err.api.message.as_deref(),
306 Error::Api(err) => err.message.as_deref(),
307 }
308 }
309
310 pub fn request_id(&self) -> Option<&str> {
311 match self {
312 Error::InvalidConfig(_) => None,
313 Error::Transport(_) => None,
314 Error::Decode(err) => err.request_id.as_deref(),
315 Error::Auth(err) => err.request_id.as_deref(),
316 Error::NotFound(err) => err.request_id.as_deref(),
317 Error::Conflict(err) => err.request_id.as_deref(),
318 Error::RateLimited(err) => err.api.request_id.as_deref(),
319 Error::Api(err) => err.request_id.as_deref(),
320 }
321 }
322
323 pub fn code(&self) -> Option<&str> {
324 match self {
325 Error::InvalidConfig(_) => None,
326 Error::Transport(_) => None,
327 Error::Decode(_) => None,
328 Error::Auth(err) => err.code.as_deref(),
329 Error::NotFound(err) => err.code.as_deref(),
330 Error::Conflict(err) => err.code.as_deref(),
331 Error::RateLimited(err) => err.api.code.as_deref(),
332 Error::Api(err) => err.code.as_deref(),
333 }
334 }
335
336 pub fn body_snippet(&self) -> Option<&str> {
337 match self {
338 Error::InvalidConfig(_) => None,
339 Error::Transport(_) => None,
340 Error::Decode(err) => err.body_snippet.as_deref(),
341 Error::Auth(err) => err.body_snippet.as_deref(),
342 Error::NotFound(err) => err.body_snippet.as_deref(),
343 Error::Conflict(err) => err.body_snippet.as_deref(),
344 Error::RateLimited(err) => err.api.body_snippet.as_deref(),
345 Error::Api(err) => err.body_snippet.as_deref(),
346 }
347 }
348
349 pub fn retry_after(&self) -> Option<Duration> {
350 match self {
351 Error::RateLimited(err) => err.retry_after,
352 _ => None,
353 }
354 }
355
356 pub fn is_retryable(&self) -> bool {
357 match self {
358 Error::RateLimited(_) => true,
359 Error::Transport(err) => is_retryable_transport_source(err.source.as_ref()),
360 Error::Api(err) => err.status.is_some_and(|status| {
361 matches!(
362 status,
363 StatusCode::BAD_GATEWAY
364 | StatusCode::SERVICE_UNAVAILABLE
365 | StatusCode::GATEWAY_TIMEOUT
366 )
367 }),
368 _ => false,
369 }
370 }
371
372 pub(crate) fn invalid_base_url(
373 base_url: impl Into<String>,
374 source: impl Into<BoxError>,
375 ) -> Self {
376 Self::InvalidConfig(Box::new(InvalidConfigError {
377 message: "invalid base url".to_string(),
378 base_url: Some(base_url.into()),
379 source: Some(source.into()),
380 }))
381 }
382
383 pub(crate) fn invalid_request_with_source(
384 message: impl Into<String>,
385 source: impl Into<BoxError>,
386 ) -> Self {
387 Self::InvalidConfig(Box::new(InvalidConfigError {
388 message: message.into(),
389 base_url: None,
390 source: Some(source.into()),
391 }))
392 }
393
394 #[cfg(feature = "async")]
395 pub(crate) fn transport_build(source: impl Into<BoxError>) -> Self {
396 Self::InvalidConfig(Box::new(InvalidConfigError {
397 message: "failed to build HTTP client".to_string(),
398 base_url: None,
399 source: Some(source.into()),
400 }))
401 }
402
403 pub(crate) fn transport(
404 method: Method,
405 host: impl Into<String>,
406 path: impl Into<String>,
407 source: impl Into<BoxError>,
408 ) -> Self {
409 Self::Transport(Box::new(TransportError {
410 method,
411 host: host.into(),
412 path: path.into(),
413 source: source.into(),
414 }))
415 }
416
417 pub(crate) fn decode(
418 status: Option<StatusCode>,
419 method: Method,
420 host: impl Into<String>,
421 path: impl Into<String>,
422 request_id: Option<String>,
423 body_snippet: Option<String>,
424 source: impl Into<BoxError>,
425 ) -> Self {
426 Self::Decode(Box::new(DecodeError {
427 status,
428 method,
429 host: host.into(),
430 path: path.into(),
431 request_id,
432 body_snippet,
433 source: source.into(),
434 }))
435 }
436
437 #[allow(clippy::too_many_arguments)]
438 pub(crate) fn api(
439 status: Option<StatusCode>,
440 method: Method,
441 host: impl Into<String>,
442 path: impl Into<String>,
443 code: Option<String>,
444 message: Option<String>,
445 request_id: Option<String>,
446 body_snippet: Option<String>,
447 retry_after: Option<Duration>,
448 ) -> Self {
449 let api = ApiError {
450 status,
451 method: Some(method),
452 host: Some(host.into()),
453 path: Some(path.into()),
454 code,
455 message,
456 request_id,
457 body_snippet,
458 };
459
460 match classify_api_error(&api) {
461 ErrorKind::Auth => Self::Auth(Box::new(api)),
462 ErrorKind::NotFound => Self::NotFound(Box::new(api)),
463 ErrorKind::Conflict => Self::Conflict(Box::new(api)),
464 ErrorKind::RateLimited => {
465 Self::RateLimited(Box::new(RateLimitedError { api, retry_after }))
466 }
467 _ => Self::Api(Box::new(api)),
468 }
469 }
470
471 pub(crate) fn signing(source: impl Into<BoxError>) -> Self {
472 Self::InvalidConfig(Box::new(InvalidConfigError {
473 message: "signing error".to_string(),
474 base_url: None,
475 source: Some(source.into()),
476 }))
477 }
478}
479
480impl fmt::Display for Error {
481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482 match self {
483 Error::InvalidConfig(err) => {
484 if let Some(base_url) = err.base_url.as_deref() {
485 write!(f, "invalid config (base url `{base_url}`): {}", err.message)
486 } else {
487 write!(f, "invalid config: {}", err.message)
488 }
489 }
490 Error::Transport(err) => write!(
491 f,
492 "transport error ({} {}{})",
493 err.method, err.host, err.path
494 ),
495 Error::Decode(err) => {
496 write!(f, "decode error ({} {}{})", err.method, err.host, err.path)?;
497 if let Some(request_id) = err.request_id.as_deref() {
498 write!(f, " (request {request_id})")?;
499 }
500 Ok(())
501 }
502 Error::Auth(err) => write_api_error(f, "auth error", err),
503 Error::NotFound(err) => write_api_error(f, "not found", err),
504 Error::Conflict(err) => write_api_error(f, "conflict", err),
505 Error::RateLimited(err) => {
506 write_api_error(f, "rate limited", &err.api)?;
507 if let Some(retry_after) = err.retry_after {
508 write!(f, " (retry after {retry_after:?})")?;
509 }
510 Ok(())
511 }
512 Error::Api(err) => write_api_error(f, "api error", err),
513 }
514 }
515}
516
517impl StdError for Error {
518 fn source(&self) -> Option<&(dyn StdError + 'static)> {
519 match self {
520 Error::InvalidConfig(err) => err.source.as_deref().map(|source| source as _),
521 Error::Transport(err) => Some(err.source.as_ref() as _),
522 Error::Decode(err) => Some(err.source.as_ref() as _),
523 _ => None,
524 }
525 }
526}
527
528fn write_api_error(f: &mut fmt::Formatter<'_>, label: &str, err: &ApiError) -> fmt::Result {
529 let status = err
530 .status
531 .map_or("<unknown>".to_string(), |status| status.to_string());
532 write!(f, "{label} (HTTP {status})")?;
533
534 if let (Some(code), Some(message)) = (err.code.as_deref(), err.message.as_deref()) {
535 write!(f, " {code}: {message}")?;
536 } else if let Some(message) = err.message.as_deref() {
537 write!(f, ": {message}")?;
538 }
539
540 if let Some(request_id) = err.request_id.as_deref() {
541 write!(f, " (request {request_id})")?;
542 }
543
544 Ok(())
545}
546
547fn classify_api_error(err: &ApiError) -> ErrorKind {
548 if err.status == Some(StatusCode::TOO_MANY_REQUESTS) {
549 return ErrorKind::RateLimited;
550 }
551
552 if matches!(
553 err.status,
554 Some(StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN)
555 ) {
556 return ErrorKind::Auth;
557 }
558
559 if err.status == Some(StatusCode::NOT_FOUND) {
560 return ErrorKind::NotFound;
561 }
562
563 if matches!(
564 err.status,
565 Some(StatusCode::CONFLICT | StatusCode::PRECONDITION_FAILED)
566 ) {
567 return ErrorKind::Conflict;
568 }
569
570 let Some(code) = err.code.as_deref() else {
571 return ErrorKind::Api;
572 };
573
574 classify_tencent_service_code(code)
575}
576
577fn classify_tencent_service_code(code: &str) -> ErrorKind {
578 if code.starts_with("AuthFailure")
579 || code.starts_with("InvalidCredential")
580 || code.starts_with("UnauthorizedOperation")
581 || code.starts_with("OperationDenied")
582 || code.starts_with("Forbidden")
583 {
584 ErrorKind::Auth
585 } else if code.starts_with("LimitExceeded")
586 || code.starts_with("RequestLimitExceeded")
587 || code.starts_with("Throttling")
588 {
589 ErrorKind::RateLimited
590 } else if code.starts_with("ResourceNotFound") {
591 ErrorKind::NotFound
592 } else if code.starts_with("ResourceInUse") || code.starts_with("ResourceUnavailable") {
593 ErrorKind::Conflict
594 } else {
595 ErrorKind::Api
596 }
597}
598
599pub(crate) fn request_id_from_headers(headers: &HeaderMap) -> Option<String> {
600 for header_name in [
601 "x-tc-requestid",
602 "x-request-id",
603 "x-requestid",
604 "x-tc-traceid",
605 ] {
606 let Some(value) = headers.get(header_name) else {
607 continue;
608 };
609 let Ok(value) = value.to_str() else {
610 continue;
611 };
612 let trimmed = value.trim();
613 if !trimmed.is_empty() {
614 return Some(trimmed.to_string());
615 }
616 }
617 None
618}
619
620fn is_retryable_transport_source(source: &(dyn StdError + 'static)) -> bool {
621 #[cfg(feature = "async")]
622 if let Some(reqwest_error) = source.downcast_ref::<reqwest::Error>() {
623 return reqwest_error.is_timeout() || reqwest_error.is_connect();
624 }
625
626 #[cfg(feature = "blocking")]
627 if let Some(ureq_error) = source.downcast_ref::<ureq::Error>() {
628 return match ureq_error {
629 ureq::Error::Timeout(_) => true,
630 ureq::Error::HostNotFound => true,
631 ureq::Error::ConnectionFailed => true,
632 ureq::Error::Tls(_) => true,
633 ureq::Error::Io(io) => matches!(
634 io.kind(),
635 std::io::ErrorKind::ConnectionReset
636 | std::io::ErrorKind::ConnectionAborted
637 | std::io::ErrorKind::ConnectionRefused
638 | std::io::ErrorKind::NotConnected
639 | std::io::ErrorKind::TimedOut
640 | std::io::ErrorKind::UnexpectedEof
641 ),
642 _ => false,
643 };
644 }
645
646 false
647}