1use http::{Method, StatusCode};
2use std::{error::Error as StdError, fmt, time::Duration};
3use thiserror::Error;
4use url::Url;
5
6pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Clone, Copy)]
9pub struct BodySnippetConfig {
10 pub enabled: bool,
11 pub max_bytes: usize,
12}
13
14impl Default for BodySnippetConfig {
15 fn default() -> Self {
16 Self {
17 enabled: true,
18 max_bytes: 4096,
19 }
20 }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum ErrorKind {
26 Auth,
27 NotFound,
28 Conflict,
29 RateLimited,
30 Api,
31 Transport,
32 Decode,
33 InvalidConfig,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[non_exhaustive]
38pub enum TransportErrorKind {
39 Timeout,
40 Connect,
41 Other,
42}
43
44#[derive(Debug, Clone)]
45pub struct HttpError {
46 pub status: StatusCode,
47 pub method: Method,
48 pub url: Box<Url>,
50 pub message: Option<Box<str>>,
51 pub request_id: Option<Box<str>>,
52 pub body_snippet: Option<Box<str>>,
53}
54
55impl HttpError {
56 #[must_use]
57 pub fn path(&self) -> &str {
58 self.url.path()
59 }
60}
61
62#[derive(Debug, Error)]
64#[non_exhaustive]
65pub enum Error {
66 #[error("{0}")]
67 Auth(HttpError),
68
69 #[error("{0}")]
70 NotFound(HttpError),
71
72 #[error("{0}")]
73 Conflict(HttpError),
74
75 #[error("{error}")]
76 RateLimited {
77 error: HttpError,
78 retry_after: Option<Duration>,
79 },
80
81 #[error("{0}")]
82 Api(HttpError),
83
84 #[error("Transport error during {method} {path}: {source}")]
85 Transport {
86 method: Method,
87 path: Box<str>,
88 kind: TransportErrorKind,
89 #[source]
90 source: Box<dyn StdError + Send + Sync>,
91 },
92
93 #[error("Decode error (HTTP {status}) during {method} {path}: {source}")]
94 Decode {
95 status: StatusCode,
96 method: Method,
97 path: Box<str>,
98 request_id: Option<Box<str>>,
99 body_snippet: Option<Box<str>>,
100 #[source]
101 source: Box<dyn StdError + Send + Sync>,
102 },
103
104 #[error("Invalid configuration: {message}")]
105 InvalidConfig {
106 message: Box<str>,
107 #[source]
108 source: Option<Box<dyn StdError + Send + Sync>>,
109 },
110}
111
112impl Error {
113 #[must_use]
114 pub fn kind(&self) -> ErrorKind {
115 match self {
116 Self::Auth(_) => ErrorKind::Auth,
117 Self::NotFound(_) => ErrorKind::NotFound,
118 Self::Conflict(_) => ErrorKind::Conflict,
119 Self::RateLimited { .. } => ErrorKind::RateLimited,
120 Self::Api(_) => ErrorKind::Api,
121 Self::Transport { .. } => ErrorKind::Transport,
122 Self::Decode { .. } => ErrorKind::Decode,
123 Self::InvalidConfig { .. } => ErrorKind::InvalidConfig,
124 }
125 }
126
127 #[must_use]
128 pub fn status(&self) -> Option<StatusCode> {
129 match self {
130 Self::Auth(e) | Self::NotFound(e) | Self::Conflict(e) | Self::Api(e) => Some(e.status),
131 Self::RateLimited { error, .. } => Some(error.status),
132 Self::Decode { status, .. } => Some(*status),
133 Self::Transport { .. } | Self::InvalidConfig { .. } => None,
134 }
135 }
136
137 #[must_use]
138 pub fn request_id(&self) -> Option<&str> {
139 match self {
140 Self::Auth(e) | Self::NotFound(e) | Self::Conflict(e) | Self::Api(e) => {
141 e.request_id.as_deref()
142 }
143 Self::RateLimited { error, .. } => error.request_id.as_deref(),
144 Self::Decode { request_id, .. } => request_id.as_deref(),
145 Self::Transport { .. } | Self::InvalidConfig { .. } => None,
146 }
147 }
148
149 #[must_use]
150 pub fn retry_after(&self) -> Option<Duration> {
151 match self {
152 Self::RateLimited { retry_after, .. } => *retry_after,
153 _ => None,
154 }
155 }
156
157 #[must_use]
158 pub fn is_auth_error(&self) -> bool {
159 matches!(self, Self::Auth(_))
160 }
161
162 #[must_use]
163 pub fn is_retryable(&self) -> bool {
164 match self {
165 Self::RateLimited { .. } => true,
166 Self::Api(e) => matches!(
167 e.status,
168 StatusCode::BAD_GATEWAY
169 | StatusCode::SERVICE_UNAVAILABLE
170 | StatusCode::GATEWAY_TIMEOUT
171 ),
172 Self::Transport { kind, .. } => matches!(
173 kind,
174 TransportErrorKind::Timeout | TransportErrorKind::Connect
175 ),
176 _ => false,
177 }
178 }
179
180 pub(crate) fn from_http(error: HttpError, retry_after: Option<Duration>) -> Self {
181 match error.status {
182 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Self::Auth(error),
183 StatusCode::NOT_FOUND => Self::NotFound(error),
184 StatusCode::CONFLICT | StatusCode::PRECONDITION_FAILED => Self::Conflict(error),
185 StatusCode::TOO_MANY_REQUESTS => Self::RateLimited { error, retry_after },
186 _ => Self::Api(error),
187 }
188 }
189}
190
191impl fmt::Display for HttpError {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 write!(f, "HTTP {} ({} {})", self.status, self.method, self.path())?;
194 if let Some(message) = self.message.as_deref() {
195 write!(f, ": {message}")?;
196 }
197 if let Some(request_id) = self.request_id.as_deref() {
198 write!(f, " [request-id: {request_id}]")?;
199 }
200 Ok(())
201 }
202}