supabase/
error.rs

1//! Error handling for the Supabase client
2
3use std::collections::HashMap;
4use thiserror::Error;
5
6/// Result type alias for Supabase operations
7#[allow(clippy::result_large_err)]
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Platform-specific error context
11#[derive(Debug, Clone)]
12pub enum PlatformContext {
13    /// Native platform (tokio runtime)
14    Native {
15        /// Operating system information
16        os_info: Option<String>,
17        /// Available system resources
18        system_resources: Option<String>,
19    },
20    /// WebAssembly platform
21    Wasm {
22        /// Browser information
23        user_agent: Option<String>,
24        /// Available Web APIs
25        available_apis: Vec<String>,
26        /// CORS status
27        cors_enabled: bool,
28    },
29}
30
31/// HTTP error details
32#[derive(Debug, Clone)]
33pub struct HttpErrorContext {
34    /// HTTP status code
35    pub status_code: Option<u16>,
36    /// Response headers
37    pub headers: Option<HashMap<String, String>>,
38    /// Response body (if available)
39    pub response_body: Option<String>,
40    /// Request URL
41    pub url: Option<String>,
42    /// Request method
43    pub method: Option<String>,
44}
45
46/// Retry information for failed requests
47#[derive(Debug, Clone)]
48pub struct RetryInfo {
49    /// Number of attempts made
50    pub attempts: u32,
51    /// Whether the error is retryable
52    pub retryable: bool,
53    /// Suggested retry delay in seconds
54    pub retry_after: Option<u64>,
55}
56
57/// Enhanced error context
58#[derive(Debug, Clone)]
59pub struct ErrorContext {
60    /// Platform-specific context
61    pub platform: Option<PlatformContext>,
62    /// HTTP error details
63    pub http: Option<HttpErrorContext>,
64    /// Retry information
65    pub retry: Option<RetryInfo>,
66    /// Additional metadata
67    pub metadata: HashMap<String, String>,
68    /// Error timestamp
69    pub timestamp: chrono::DateTime<chrono::Utc>,
70}
71
72impl Default for ErrorContext {
73    fn default() -> Self {
74        Self {
75            platform: None,
76            http: None,
77            retry: None,
78            metadata: HashMap::new(),
79            timestamp: chrono::Utc::now(),
80        }
81    }
82}
83
84/// Main error type for Supabase operations
85#[derive(Error, Debug)]
86pub enum Error {
87    /// HTTP request errors with enhanced context
88    #[error("HTTP request failed: {message}")]
89    Http {
90        message: String,
91        #[source]
92        source: Option<reqwest::Error>,
93        context: ErrorContext,
94    },
95
96    /// JSON serialization/deserialization errors
97    #[error("JSON error: {0}")]
98    Json(#[from] serde_json::Error),
99
100    /// URL parsing errors
101    #[error("URL parse error: {0}")]
102    UrlParse(#[from] url::ParseError),
103
104    /// JWT token errors
105    #[cfg(feature = "auth")]
106    #[error("JWT error: {0}")]
107    Jwt(#[from] jsonwebtoken::errors::Error),
108
109    /// Authentication errors with enhanced context
110    #[error("Authentication error: {message}")]
111    Auth {
112        message: String,
113        context: ErrorContext,
114    },
115
116    /// Database operation errors with enhanced context
117    #[error("Database error: {message}")]
118    Database {
119        message: String,
120        context: ErrorContext,
121    },
122
123    /// Storage operation errors with enhanced context
124    #[error("Storage error: {message}")]
125    Storage {
126        message: String,
127        context: ErrorContext,
128    },
129
130    /// Realtime connection errors with enhanced context
131    #[error("Realtime error: {message}")]
132    Realtime {
133        message: String,
134        context: ErrorContext,
135    },
136
137    /// Configuration errors
138    #[error("Configuration error: {message}")]
139    Config { message: String },
140
141    /// Invalid input errors
142    #[error("Invalid input: {message}")]
143    InvalidInput { message: String },
144
145    /// Network errors with enhanced context
146    #[error("Network error: {message}")]
147    Network {
148        message: String,
149        context: ErrorContext,
150    },
151
152    /// Rate limiting errors with retry information
153    #[error("Rate limit exceeded: {message}")]
154    RateLimit {
155        message: String,
156        context: ErrorContext,
157    },
158
159    /// Permission denied errors with enhanced context
160    #[error("Permission denied: {message}")]
161    PermissionDenied {
162        message: String,
163        context: ErrorContext,
164    },
165
166    /// Resource not found errors with enhanced context
167    #[error("Not found: {message}")]
168    NotFound {
169        message: String,
170        context: ErrorContext,
171    },
172
173    /// Generic errors
174    #[error("{message}")]
175    Generic { message: String },
176
177    /// Functions errors with enhanced context
178    #[error("Functions error: {message}")]
179    Functions {
180        message: String,
181        context: ErrorContext,
182    },
183}
184
185impl Error {
186    /// Create an authentication error with enhanced context
187    pub fn auth<S: Into<String>>(message: S) -> Self {
188        Self::Auth {
189            message: message.into(),
190            context: ErrorContext::default(),
191        }
192    }
193
194    /// Create an authentication error with custom context
195    pub fn auth_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
196        Self::Auth {
197            message: message.into(),
198            context,
199        }
200    }
201
202    /// Create a database error with enhanced context
203    pub fn database<S: Into<String>>(message: S) -> Self {
204        Self::Database {
205            message: message.into(),
206            context: ErrorContext::default(),
207        }
208    }
209
210    /// Create a database error with custom context
211    pub fn database_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
212        Self::Database {
213            message: message.into(),
214            context,
215        }
216    }
217
218    /// Create a storage error with enhanced context
219    pub fn storage<S: Into<String>>(message: S) -> Self {
220        Self::Storage {
221            message: message.into(),
222            context: ErrorContext::default(),
223        }
224    }
225
226    /// Create a storage error with custom context
227    pub fn storage_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
228        Self::Storage {
229            message: message.into(),
230            context,
231        }
232    }
233
234    /// Create a realtime error with enhanced context
235    pub fn realtime<S: Into<String>>(message: S) -> Self {
236        Self::Realtime {
237            message: message.into(),
238            context: ErrorContext::default(),
239        }
240    }
241
242    /// Create a realtime error with custom context
243    pub fn realtime_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
244        Self::Realtime {
245            message: message.into(),
246            context,
247        }
248    }
249
250    /// Create a functions error with enhanced context
251    pub fn functions<S: Into<String>>(message: S) -> Self {
252        Self::Functions {
253            message: message.into(),
254            context: ErrorContext::default(),
255        }
256    }
257
258    /// Create a functions error with custom context
259    pub fn functions_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
260        Self::Functions {
261            message: message.into(),
262            context,
263        }
264    }
265
266    /// Create a network error with enhanced context
267    pub fn network<S: Into<String>>(message: S) -> Self {
268        Self::Network {
269            message: message.into(),
270            context: ErrorContext::default(),
271        }
272    }
273
274    /// Create a rate limit error with retry information
275    pub fn rate_limit<S: Into<String>>(message: S, retry_after: Option<u64>) -> Self {
276        let context = ErrorContext {
277            retry: Some(RetryInfo {
278                attempts: 0,
279                retryable: true,
280                retry_after,
281            }),
282            ..Default::default()
283        };
284
285        Self::RateLimit {
286            message: message.into(),
287            context,
288        }
289    }
290
291    /// Create a permission denied error with enhanced context
292    pub fn permission_denied<S: Into<String>>(message: S) -> Self {
293        Self::PermissionDenied {
294            message: message.into(),
295            context: ErrorContext::default(),
296        }
297    }
298
299    /// Create a not found error with enhanced context
300    pub fn not_found<S: Into<String>>(message: S) -> Self {
301        Self::NotFound {
302            message: message.into(),
303            context: ErrorContext::default(),
304        }
305    }
306
307    /// Create a configuration error
308    pub fn config<S: Into<String>>(message: S) -> Self {
309        Self::Config {
310            message: message.into(),
311        }
312    }
313
314    /// Create an invalid input error
315    pub fn invalid_input<S: Into<String>>(message: S) -> Self {
316        Self::InvalidInput {
317            message: message.into(),
318        }
319    }
320
321    /// Create a generic error
322    pub fn generic<S: Into<String>>(message: S) -> Self {
323        Self::Generic {
324            message: message.into(),
325        }
326    }
327
328    /// Get error context if available
329    pub fn context(&self) -> Option<&ErrorContext> {
330        match self {
331            Error::Http { context, .. } => Some(context),
332            Error::Auth { context, .. } => Some(context),
333            Error::Database { context, .. } => Some(context),
334            Error::Storage { context, .. } => Some(context),
335            Error::Realtime { context, .. } => Some(context),
336            Error::Network { context, .. } => Some(context),
337            Error::RateLimit { context, .. } => Some(context),
338            Error::PermissionDenied { context, .. } => Some(context),
339            Error::NotFound { context, .. } => Some(context),
340            Error::Functions { context, .. } => Some(context),
341            _ => None,
342        }
343    }
344
345    /// Check if error is retryable
346    pub fn is_retryable(&self) -> bool {
347        self.context()
348            .and_then(|ctx| ctx.retry.as_ref())
349            .map(|retry| retry.retryable)
350            .unwrap_or(false)
351    }
352
353    /// Get retry delay in seconds
354    pub fn retry_after(&self) -> Option<u64> {
355        self.context()
356            .and_then(|ctx| ctx.retry.as_ref())
357            .and_then(|retry| retry.retry_after)
358    }
359
360    /// Get HTTP status code if available
361    pub fn status_code(&self) -> Option<u16> {
362        self.context()
363            .and_then(|ctx| ctx.http.as_ref())
364            .and_then(|http| http.status_code)
365    }
366}
367
368/// Detect current platform context
369fn detect_platform_context() -> PlatformContext {
370    #[cfg(target_arch = "wasm32")]
371    {
372        PlatformContext::Wasm {
373            user_agent: web_sys::window().and_then(|window| window.navigator().user_agent().ok()),
374            available_apis: detect_available_web_apis(),
375            cors_enabled: true, // Assume CORS is enabled for simplicity
376        }
377    }
378
379    #[cfg(not(target_arch = "wasm32"))]
380    {
381        PlatformContext::Native {
382            os_info: Some(format!(
383                "{} {}",
384                std::env::consts::OS,
385                std::env::consts::ARCH
386            )),
387            system_resources: None, // Could be enhanced with system info
388        }
389    }
390}
391
392/// Detect available Web APIs in WASM environment
393#[cfg(target_arch = "wasm32")]
394#[allow(dead_code)]
395fn detect_available_web_apis() -> Vec<String> {
396    let mut apis = Vec::new();
397
398    if let Some(window) = web_sys::window() {
399        // Check for common Web APIs
400        if window.local_storage().is_ok() {
401            apis.push("localStorage".to_string());
402        }
403        if window.session_storage().is_ok() {
404            apis.push("sessionStorage".to_string());
405        }
406        apis.push("fetch".to_string()); // Fetch API is generally available
407    }
408
409    apis
410}
411
412#[cfg(not(target_arch = "wasm32"))]
413#[allow(dead_code)]
414fn detect_available_web_apis() -> Vec<String> {
415    Vec::new()
416}
417
418impl From<reqwest::Error> for Error {
419    fn from(err: reqwest::Error) -> Self {
420        let mut context = ErrorContext::default();
421
422        // Add HTTP context if available
423        if let Some(status) = err.status() {
424            context.http = Some(HttpErrorContext {
425                status_code: Some(status.as_u16()),
426                headers: None,
427                response_body: None,
428                url: err.url().map(|u| u.to_string()),
429                method: None,
430            });
431
432            // Determine if error is retryable
433            let retryable = match status.as_u16() {
434                500..=599 | 429 | 408 => true, // Server errors, rate limit, timeout
435                _ => false,
436            };
437
438            context.retry = Some(RetryInfo {
439                attempts: 0,
440                retryable,
441                retry_after: None,
442            });
443        }
444
445        // Add platform context
446        context.platform = Some(detect_platform_context());
447
448        Error::Http {
449            message: err.to_string(),
450            source: Some(err),
451            context,
452        }
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn test_error_creation() {
462        let error = Error::auth("test message");
463        assert_eq!(error.to_string(), "Authentication error: test message");
464    }
465
466    #[test]
467    fn test_database_error() {
468        let error = Error::database("query failed");
469        assert_eq!(error.to_string(), "Database error: query failed");
470    }
471
472    #[test]
473    fn test_error_context() {
474        let error = Error::auth("test message");
475        assert!(error.context().is_some());
476        if let Some(context) = error.context() {
477            assert!(context.timestamp <= chrono::Utc::now());
478        }
479    }
480}