1use std::fmt;
4use std::time::Duration;
5
6use serde_json::Value;
7
8use crate::response::{RunStatus, SmartBrowseRun};
9
10pub type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum Error {
17 #[error(transparent)]
19 Api(#[from] ApiError),
20
21 #[error("transport error: {0}")]
24 Transport(#[source] reqwest::Error),
25
26 #[error("request timed out after {0:?}")]
28 Timeout(Duration),
29
30 #[error("smartbrowse run {} ended in state {}", .run.id, .run.run_status)]
34 RunFailed {
35 run: SmartBrowseRun,
37 },
38
39 #[error("timed out waiting for smartbrowse run {} (last state: {})", .last.id, .last.run_status)]
42 WaitTimeout {
43 last: SmartBrowseRun,
45 },
46
47 #[error("configuration error: {0}")]
49 Config(String),
50
51 #[error("decode error: {0}")]
53 Decode(String),
54}
55
56impl Error {
57 pub fn as_api(&self) -> Option<&ApiError> {
59 match self {
60 Error::Api(e) => Some(e),
61 _ => None,
62 }
63 }
64
65 pub fn is_code(&self, code: &ErrorCode) -> bool {
67 self.as_api().map(|e| e.code() == code).unwrap_or(false)
68 }
69
70 pub fn is_unauthorized(&self) -> bool {
72 self.as_api()
73 .map(ApiError::is_unauthorized)
74 .unwrap_or(false)
75 }
76
77 pub fn is_insufficient_credits(&self) -> bool {
79 self.as_api()
80 .map(ApiError::is_insufficient_credits)
81 .unwrap_or(false)
82 }
83
84 pub fn is_rate_limited(&self) -> bool {
86 self.as_api()
87 .map(ApiError::is_rate_limited)
88 .unwrap_or(false)
89 }
90
91 pub fn is_not_found(&self) -> bool {
93 self.as_api().map(ApiError::is_not_found).unwrap_or(false)
94 }
95}
96
97#[derive(Debug, Clone, thiserror::Error)]
102#[error("api error [{status}] {code}: {message}")]
103pub struct ApiError {
104 status: u16,
105 code: ErrorCode,
106 message: String,
107 details: Option<Value>,
108 request_id: Option<String>,
109 retry_after: Option<Duration>,
110}
111
112impl ApiError {
113 #[allow(clippy::too_many_arguments)]
114 pub(crate) fn new(
115 status: u16,
116 code: ErrorCode,
117 message: String,
118 details: Option<Value>,
119 request_id: Option<String>,
120 retry_after: Option<Duration>,
121 ) -> Self {
122 Self {
123 status,
124 code,
125 message,
126 details,
127 request_id,
128 retry_after,
129 }
130 }
131
132 pub fn status(&self) -> u16 {
134 self.status
135 }
136
137 pub fn code(&self) -> &ErrorCode {
139 &self.code
140 }
141
142 pub fn kind(&self) -> &ErrorCode {
144 &self.code
145 }
146
147 pub fn message(&self) -> &str {
149 &self.message
150 }
151
152 pub fn details(&self) -> Option<&Value> {
154 self.details.as_ref()
155 }
156
157 pub fn request_id(&self) -> Option<&str> {
160 self.request_id.as_deref()
161 }
162
163 pub fn retry_after(&self) -> Option<Duration> {
165 self.retry_after
166 }
167
168 pub fn balance(&self) -> Option<i64> {
170 self.detail_i64("balance")
171 }
172
173 pub fn required(&self) -> Option<i64> {
175 self.detail_i64("required")
176 }
177
178 pub fn reason(&self) -> Option<&str> {
181 self.detail_str("reason")
182 }
183
184 pub fn deletion_scheduled_for(&self) -> Option<&str> {
186 self.detail_str("deletion_scheduled_for")
187 }
188
189 pub fn is_unauthorized(&self) -> bool {
191 matches!(self.code, ErrorCode::Unauthorized)
192 }
193
194 pub fn is_insufficient_credits(&self) -> bool {
196 matches!(self.code, ErrorCode::InsufficientCredits)
197 }
198
199 pub fn is_rate_limited(&self) -> bool {
201 matches!(self.code, ErrorCode::RateLimited)
202 }
203
204 pub fn is_not_found(&self) -> bool {
206 matches!(self.code, ErrorCode::NotFound)
207 }
208
209 pub fn is_validation_failed(&self) -> bool {
211 matches!(self.code, ErrorCode::ValidationFailed)
212 }
213
214 fn detail_i64(&self, key: &str) -> Option<i64> {
215 self.details.as_ref()?.get(key)?.as_i64()
216 }
217
218 fn detail_str(&self, key: &str) -> Option<&str> {
219 self.details.as_ref()?.get(key)?.as_str()
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
226#[non_exhaustive]
227pub enum ErrorCode {
228 InvalidRequest,
230 Unauthorized,
232 InsufficientCredits,
234 EmailVerificationRequired,
236 Forbidden,
238 NotFound,
240 Conflict,
242 AccountDeletionPending,
244 ValidationFailed,
246 RateLimited,
248 InternalError,
250 ServiceUnavailable,
252 Unknown(String),
254}
255
256impl ErrorCode {
257 pub fn as_str(&self) -> &str {
259 match self {
260 ErrorCode::InvalidRequest => "invalid_request",
261 ErrorCode::Unauthorized => "unauthorized",
262 ErrorCode::InsufficientCredits => "insufficient_credits",
263 ErrorCode::EmailVerificationRequired => "email_verification_required",
264 ErrorCode::Forbidden => "forbidden",
265 ErrorCode::NotFound => "not_found",
266 ErrorCode::Conflict => "conflict",
267 ErrorCode::AccountDeletionPending => "account_deletion_pending",
268 ErrorCode::ValidationFailed => "validation_failed",
269 ErrorCode::RateLimited => "rate_limited",
270 ErrorCode::InternalError => "internal_error",
271 ErrorCode::ServiceUnavailable => "service_unavailable",
272 ErrorCode::Unknown(s) => s.as_str(),
273 }
274 }
275
276 pub(crate) fn parse(s: &str) -> Self {
279 match s {
280 "invalid_request" => ErrorCode::InvalidRequest,
281 "unauthorized" => ErrorCode::Unauthorized,
282 "insufficient_credits" => ErrorCode::InsufficientCredits,
283 "email_verification_required" => ErrorCode::EmailVerificationRequired,
284 "forbidden" => ErrorCode::Forbidden,
285 "not_found" => ErrorCode::NotFound,
286 "conflict" => ErrorCode::Conflict,
287 "account_deletion_pending" => ErrorCode::AccountDeletionPending,
288 "validation_failed" => ErrorCode::ValidationFailed,
289 "rate_limited" => ErrorCode::RateLimited,
290 "internal_error" => ErrorCode::InternalError,
291 "service_unavailable" => ErrorCode::ServiceUnavailable,
292 other => ErrorCode::Unknown(other.to_string()),
293 }
294 }
295
296 pub(crate) fn from_status(status: u16) -> Self {
299 match status {
300 400 => ErrorCode::InvalidRequest,
301 401 => ErrorCode::Unauthorized,
302 403 => ErrorCode::Forbidden,
303 404 => ErrorCode::NotFound,
304 409 => ErrorCode::Conflict,
305 422 => ErrorCode::ValidationFailed,
306 429 => ErrorCode::RateLimited,
307 500 => ErrorCode::InternalError,
308 502 | 503 => ErrorCode::ServiceUnavailable,
309 other => ErrorCode::Unknown(format!("http_{other}")),
310 }
311 }
312}
313
314impl fmt::Display for ErrorCode {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 f.write_str(self.as_str())
317 }
318}
319
320impl fmt::Display for RunStatus {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 let s = match self {
323 RunStatus::Queued => "queued",
324 RunStatus::Running => "running",
325 RunStatus::Completed => "completed",
326 RunStatus::Failed => "failed",
327 RunStatus::Cancelled => "cancelled",
328 RunStatus::Unknown => "unknown",
329 };
330 f.write_str(s)
331 }
332}