tencent_sdk/
error.rs

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}