Skip to main content

edgequake_sdk/
error.rs

1//! Error types for the EdgeQuake SDK.
2
3use std::time::Duration;
4use thiserror::Error;
5
6/// All errors that can occur when using the EdgeQuake SDK.
7#[derive(Error, Debug)]
8pub enum Error {
9    /// 400 Bad Request
10    #[error("Bad request: {message}")]
11    BadRequest {
12        message: String,
13        code: Option<String>,
14        details: Option<serde_json::Value>,
15    },
16
17    /// 401 Unauthorized
18    #[error("Unauthorized: {message}")]
19    Unauthorized { message: String },
20
21    /// 403 Forbidden
22    #[error("Forbidden: {message}")]
23    Forbidden { message: String },
24
25    /// 404 Not Found
26    #[error("Not found: {message}")]
27    NotFound { message: String },
28
29    /// 409 Conflict
30    #[error("Conflict: {message}")]
31    Conflict { message: String },
32
33    /// 422 Unprocessable Entity
34    #[error("Validation error: {message}")]
35    Validation {
36        message: String,
37        details: Option<serde_json::Value>,
38    },
39
40    /// 429 Rate Limited
41    #[error("Rate limited (retry after {retry_after:?})")]
42    RateLimited {
43        message: String,
44        retry_after: Option<Duration>,
45    },
46
47    /// 500+ Server Error
48    #[error("Server error ({status}): {message}")]
49    Server {
50        status: u16,
51        message: String,
52        code: Option<String>,
53    },
54
55    /// Network/transport error
56    #[error("Network error: {0}")]
57    Network(#[from] reqwest::Error),
58
59    /// JSON serialization error
60    #[error("JSON error: {0}")]
61    Json(#[from] serde_json::Error),
62
63    /// URL parsing error
64    #[error("URL error: {0}")]
65    Url(#[from] url::ParseError),
66
67    /// Configuration error
68    #[error("Configuration error: {0}")]
69    Config(String),
70
71    /// Timeout
72    #[error("Timeout after {duration:?} waiting for {operation}")]
73    Timeout {
74        operation: String,
75        duration: Duration,
76    },
77}
78
79/// Convenience Result alias.
80pub type Result<T> = std::result::Result<T, Error>;
81
82/// Server error response body.
83#[derive(Debug, Clone, serde::Deserialize)]
84pub(crate) struct ErrorResponse {
85    #[serde(default)]
86    pub code: String,
87    #[serde(default)]
88    pub message: String,
89    #[serde(default)]
90    pub details: Option<serde_json::Value>,
91}
92
93impl Error {
94    /// Convert an HTTP response into the appropriate error variant.
95    pub(crate) async fn from_response(resp: reqwest::Response) -> Self {
96        let status = resp.status().as_u16();
97        let retry_after = resp
98            .headers()
99            .get("retry-after")
100            .and_then(|v| v.to_str().ok())
101            .and_then(|v| v.parse::<u64>().ok())
102            .map(Duration::from_secs);
103
104        let body = resp.json::<ErrorResponse>().await.ok();
105        let message = body
106            .as_ref()
107            .map(|b| b.message.clone())
108            .unwrap_or_else(|| format!("HTTP {status}"));
109        let code = body.as_ref().map(|b| b.code.clone());
110        let details = body.as_ref().and_then(|b| b.details.clone());
111
112        match status {
113            400 => Error::BadRequest {
114                message,
115                code,
116                details,
117            },
118            401 => Error::Unauthorized { message },
119            403 => Error::Forbidden { message },
120            404 => Error::NotFound { message },
121            409 => Error::Conflict { message },
122            422 => Error::Validation {
123                message,
124                details,
125            },
126            429 => Error::RateLimited {
127                message,
128                retry_after,
129            },
130            _ => Error::Server {
131                status,
132                message,
133                code,
134            },
135        }
136    }
137
138    /// Whether this error is retryable.
139    pub fn is_retryable(&self) -> bool {
140        matches!(
141            self,
142            Error::RateLimited { .. }
143                | Error::Server {
144                    status: 500 | 502 | 503 | 504,
145                    ..
146                }
147                | Error::Network(_)
148        )
149    }
150
151    /// HTTP status code if this is an API error.
152    pub fn status_code(&self) -> Option<u16> {
153        match self {
154            Error::BadRequest { .. } => Some(400),
155            Error::Unauthorized { .. } => Some(401),
156            Error::Forbidden { .. } => Some(403),
157            Error::NotFound { .. } => Some(404),
158            Error::Conflict { .. } => Some(409),
159            Error::Validation { .. } => Some(422),
160            Error::RateLimited { .. } => Some(429),
161            Error::Server { status, .. } => Some(*status),
162            _ => None,
163        }
164    }
165}