rainy_sdk/error.rs
1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4/// The comprehensive error type for all operations within the Rainy SDK.
5///
6/// `RainyError` is an enumeration of all possible errors that can occur,
7/// providing detailed context for each error variant.
8#[derive(Error, Debug, Clone)]
9pub enum RainyError {
10 /// An error related to authentication, such as an invalid or expired API key.
11 #[error("Authentication failed: {message}")]
12 Authentication {
13 /// A machine-readable error code (e.g., `INVALID_API_KEY`).
14 code: String,
15 /// A human-readable error message.
16 message: String,
17 /// Indicates whether the request can be retried.
18 retryable: bool,
19 },
20
21 /// An error due to an invalid request, such as a missing required field.
22 #[error("Invalid request: {message}")]
23 InvalidRequest {
24 /// A machine-readable error code (e.g., `MISSING_REQUIRED_FIELD`).
25 code: String,
26 /// A human-readable error message.
27 message: String,
28 /// Additional details about the error, if available.
29 details: Option<serde_json::Value>,
30 },
31
32 /// An error that originates from an underlying AI provider (e.g., OpenAI, Anthropic).
33 #[error("Provider error ({provider}): {message}")]
34 Provider {
35 /// The error code from the provider.
36 code: String,
37 /// The error message from the provider.
38 message: String,
39 /// The name of the provider that returned the error.
40 provider: String,
41 /// Indicates whether the request can be retried.
42 retryable: bool,
43 },
44
45 /// An error indicating that the rate limit for the API has been exceeded.
46 #[error("Rate limit exceeded: {message}")]
47 RateLimit {
48 /// A machine-readable error code (e.g., `RATE_LIMIT_EXCEEDED`).
49 code: String,
50 /// A human-readable error message.
51 message: String,
52 /// The recommended time to wait before retrying, in seconds.
53 retry_after: Option<u64>,
54 /// Information about the current usage, if available.
55 current_usage: Option<String>,
56 },
57
58 /// An error indicating that the account has insufficient credits to perform the request.
59 #[error("Insufficient credits: {message}")]
60 InsufficientCredits {
61 /// A machine-readable error code (e.g., `INSUFFICIENT_CREDITS`).
62 code: String,
63 /// A human-readable error message.
64 message: String,
65 /// The current credit balance of the account.
66 current_credits: f64,
67 /// The estimated cost of the request.
68 estimated_cost: f64,
69 /// The date when the credits are scheduled to be reset or topped up.
70 reset_date: Option<String>,
71 },
72
73 /// An error related to network connectivity or HTTP-level issues.
74 #[error("Network error: {message}")]
75 Network {
76 /// A message describing the network error.
77 message: String,
78 /// Indicates whether the request can be retried.
79 retryable: bool,
80 /// The underlying error message, if available.
81 source_error: Option<String>,
82 },
83
84 /// A generic API error that doesn't fit into the other categories.
85 #[error("API error [{status_code}]: {message}")]
86 Api {
87 /// A machine-readable error code.
88 code: String,
89 /// A human-readable error message.
90 message: String,
91 /// The HTTP status code of the response.
92 status_code: u16,
93 /// Indicates whether the request can be retried.
94 retryable: bool,
95 /// The unique ID of the request, for debugging purposes.
96 request_id: Option<String>,
97 },
98
99 /// An error indicating that the request timed out.
100 #[error("Request timeout: {message}")]
101 Timeout {
102 /// A message describing the timeout.
103 message: String,
104 /// The timeout duration in milliseconds.
105 duration_ms: u64,
106 },
107
108 /// An error that occurs during serialization or deserialization of data.
109 #[error("Serialization error: {message}")]
110 Serialization {
111 /// A message describing the serialization error.
112 message: String,
113 /// The underlying error message, if available.
114 source_error: Option<String>,
115 },
116}
117
118impl RainyError {
119 /// Checks if the error is considered retryable.
120 ///
121 /// Some errors, like network issues or rate limiting, are transient and can be resolved
122 /// by retrying the request.
123 ///
124 /// # Returns
125 ///
126 /// `true` if the error is retryable, `false` otherwise.
127 pub fn is_retryable(&self) -> bool {
128 match self {
129 RainyError::Authentication { retryable, .. } => *retryable,
130 RainyError::Provider { retryable, .. } => *retryable,
131 RainyError::Network { retryable, .. } => *retryable,
132 RainyError::Api { retryable, .. } => *retryable,
133 RainyError::RateLimit { .. } => true,
134 RainyError::Timeout { .. } => true,
135 _ => false,
136 }
137 }
138
139 /// Returns the recommended delay in seconds before a retry, if applicable.
140 ///
141 /// This is typically used with `RateLimit` errors.
142 ///
143 /// # Returns
144 ///
145 /// An `Option<u64>` containing the retry delay in seconds, or `None` if not applicable.
146 pub fn retry_after(&self) -> Option<u64> {
147 match self {
148 RainyError::RateLimit { retry_after, .. } => *retry_after,
149 _ => None,
150 }
151 }
152
153 /// Returns the machine-readable error code, if available.
154 pub fn code(&self) -> Option<&str> {
155 match self {
156 RainyError::Authentication { code, .. }
157 | RainyError::InvalidRequest { code, .. }
158 | RainyError::Provider { code, .. }
159 | RainyError::RateLimit { code, .. }
160 | RainyError::InsufficientCredits { code, .. }
161 | RainyError::Api { code, .. } => Some(code),
162 _ => None,
163 }
164 }
165
166 /// Returns the unique request ID associated with the error, if available.
167 ///
168 /// This is useful for debugging and support requests.
169 pub fn request_id(&self) -> Option<&str> {
170 match self {
171 RainyError::Api { request_id, .. } => request_id.as_deref(),
172 _ => None,
173 }
174 }
175}
176
177/// The structure of a standard error response from the Rainy API.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ApiErrorResponse {
180 /// The detailed error information.
181 pub error: ApiErrorDetails,
182}
183
184/// Detailed information about an API error.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ApiErrorDetails {
187 /// A machine-readable error code.
188 pub code: String,
189 /// A human-readable error message.
190 pub message: String,
191 /// Additional, structured details about the error.
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub details: Option<serde_json::Value>,
194 /// Indicates whether the request that caused this error can be retried.
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub retryable: Option<bool>,
197 /// The timestamp of when the error occurred.
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub timestamp: Option<String>,
200 /// The unique ID of the request.
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub request_id: Option<String>,
203}
204
205/// A convenience type alias for `Result<T, RainyError>`.
206pub type Result<T> = std::result::Result<T, RainyError>;
207
208/// Converts a `reqwest::Error` into a `RainyError`.
209///
210/// This implementation categorizes `reqwest` errors into `Timeout`, `Network`,
211/// or other appropriate `RainyError` variants.
212impl From<reqwest::Error> for RainyError {
213 fn from(err: reqwest::Error) -> Self {
214 if err.is_timeout() {
215 RainyError::Timeout {
216 message: "Request timed out".to_string(),
217 duration_ms: 30000, // Default timeout
218 }
219 } else if err.is_connect() || err.is_request() {
220 RainyError::Network {
221 message: err.to_string(),
222 retryable: true,
223 source_error: Some(err.to_string()),
224 }
225 } else {
226 RainyError::Network {
227 message: err.to_string(),
228 retryable: false,
229 source_error: Some(err.to_string()),
230 }
231 }
232 }
233}
234
235/// Converts a `serde_json::Error` into a `RainyError`.
236///
237/// This is used for errors that occur during the serialization or deserialization of JSON data.
238impl From<serde_json::Error> for RainyError {
239 fn from(err: serde_json::Error) -> Self {
240 RainyError::Serialization {
241 message: err.to_string(),
242 source_error: Some(err.to_string()),
243 }
244 }
245}
246
247/// Converts a `reqwest::header::InvalidHeaderValue` into a `RainyError`.
248///
249/// This is used when an invalid value is provided for an HTTP header.
250impl From<reqwest::header::InvalidHeaderValue> for RainyError {
251 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
252 RainyError::InvalidRequest {
253 code: "INVALID_HEADER".to_string(),
254 message: format!("Invalid header value: {}", err),
255 details: None,
256 }
257 }
258}