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    /// Platform-specific error
185    #[error("Platform error: {message}")]
186    Platform {
187        message: String,
188        context: ErrorContext,
189    },
190
191    /// Cryptographic error
192    #[error("Crypto error: {message}")]
193    Crypto {
194        message: String,
195        context: ErrorContext,
196    },
197}
198
199impl Error {
200    /// Create an authentication error with enhanced context
201    pub fn auth<S: Into<String>>(message: S) -> Self {
202        Self::Auth {
203            message: message.into(),
204            context: ErrorContext::default(),
205        }
206    }
207
208    /// Create an authentication error with custom context
209    pub fn auth_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
210        Self::Auth {
211            message: message.into(),
212            context,
213        }
214    }
215
216    /// Create a database error with enhanced context
217    pub fn database<S: Into<String>>(message: S) -> Self {
218        Self::Database {
219            message: message.into(),
220            context: ErrorContext::default(),
221        }
222    }
223
224    /// Create a database error with custom context
225    pub fn database_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
226        Self::Database {
227            message: message.into(),
228            context,
229        }
230    }
231
232    /// Create a storage error with enhanced context
233    pub fn storage<S: Into<String>>(message: S) -> Self {
234        Self::Storage {
235            message: message.into(),
236            context: ErrorContext::default(),
237        }
238    }
239
240    /// Create a storage error with custom context
241    pub fn storage_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
242        Self::Storage {
243            message: message.into(),
244            context,
245        }
246    }
247
248    /// Create a realtime error with enhanced context
249    pub fn realtime<S: Into<String>>(message: S) -> Self {
250        Self::Realtime {
251            message: message.into(),
252            context: ErrorContext::default(),
253        }
254    }
255
256    /// Create a realtime error with custom context
257    pub fn realtime_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
258        Self::Realtime {
259            message: message.into(),
260            context,
261        }
262    }
263
264    /// Create a functions error with enhanced context
265    pub fn functions<S: Into<String>>(message: S) -> Self {
266        Self::Functions {
267            message: message.into(),
268            context: ErrorContext::default(),
269        }
270    }
271
272    /// Create a functions error with custom context
273    pub fn functions_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
274        Self::Functions {
275            message: message.into(),
276            context,
277        }
278    }
279
280    /// Create a network error with enhanced context
281    pub fn network<S: Into<String>>(message: S) -> Self {
282        Self::Network {
283            message: message.into(),
284            context: ErrorContext::default(),
285        }
286    }
287
288    /// Create a rate limit error with retry information
289    pub fn rate_limit<S: Into<String>>(message: S, retry_after: Option<u64>) -> Self {
290        let context = ErrorContext {
291            retry: Some(RetryInfo {
292                attempts: 0,
293                retryable: true,
294                retry_after,
295            }),
296            ..Default::default()
297        };
298
299        Self::RateLimit {
300            message: message.into(),
301            context,
302        }
303    }
304
305    /// Create a permission denied error with enhanced context
306    pub fn permission_denied<S: Into<String>>(message: S) -> Self {
307        Self::PermissionDenied {
308            message: message.into(),
309            context: ErrorContext::default(),
310        }
311    }
312
313    /// Create a not found error with enhanced context
314    pub fn not_found<S: Into<String>>(message: S) -> Self {
315        Self::NotFound {
316            message: message.into(),
317            context: ErrorContext::default(),
318        }
319    }
320
321    /// Create a configuration error
322    pub fn config<S: Into<String>>(message: S) -> Self {
323        Self::Config {
324            message: message.into(),
325        }
326    }
327
328    /// Create an invalid input error
329    pub fn invalid_input<S: Into<String>>(message: S) -> Self {
330        Self::InvalidInput {
331            message: message.into(),
332        }
333    }
334
335    /// Create a generic error
336    pub fn generic<S: Into<String>>(message: S) -> Self {
337        Self::Generic {
338            message: message.into(),
339        }
340    }
341
342    /// Get error context if available
343    pub fn context(&self) -> Option<&ErrorContext> {
344        match self {
345            Error::Http { context, .. } => Some(context),
346            Error::Auth { context, .. } => Some(context),
347            Error::Database { context, .. } => Some(context),
348            Error::Storage { context, .. } => Some(context),
349            Error::Realtime { context, .. } => Some(context),
350            Error::Network { context, .. } => Some(context),
351            Error::RateLimit { context, .. } => Some(context),
352            Error::PermissionDenied { context, .. } => Some(context),
353            Error::NotFound { context, .. } => Some(context),
354            Error::Functions { context, .. } => Some(context),
355            _ => None,
356        }
357    }
358
359    /// Check if error is retryable
360    pub fn is_retryable(&self) -> bool {
361        self.context()
362            .and_then(|ctx| ctx.retry.as_ref())
363            .map(|retry| retry.retryable)
364            .unwrap_or(false)
365    }
366
367    /// Get retry delay in seconds
368    pub fn retry_after(&self) -> Option<u64> {
369        self.context()
370            .and_then(|ctx| ctx.retry.as_ref())
371            .and_then(|retry| retry.retry_after)
372    }
373
374    /// Get HTTP status code if available
375    pub fn status_code(&self) -> Option<u16> {
376        self.context()
377            .and_then(|ctx| ctx.http.as_ref())
378            .and_then(|http| http.status_code)
379    }
380
381    /// Create a platform error
382    pub fn platform<S: Into<String>>(message: S) -> Self {
383        Self::Platform {
384            message: message.into(),
385            context: ErrorContext::default(),
386        }
387    }
388
389    /// Create a platform error with context
390    pub fn platform_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
391        Self::Platform {
392            message: message.into(),
393            context,
394        }
395    }
396
397    /// Create a cryptographic error
398    pub fn crypto<S: Into<String>>(message: S) -> Self {
399        Self::Crypto {
400            message: message.into(),
401            context: ErrorContext::default(),
402        }
403    }
404
405    /// Create a cryptographic error with context
406    pub fn crypto_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
407        Self::Crypto {
408            message: message.into(),
409            context,
410        }
411    }
412}
413
414/// Detect current platform context
415pub fn detect_platform_context() -> PlatformContext {
416    #[cfg(target_arch = "wasm32")]
417    {
418        PlatformContext::Wasm {
419            user_agent: web_sys::window().and_then(|window| window.navigator().user_agent().ok()),
420            available_apis: detect_available_web_apis(),
421            cors_enabled: true, // Assume CORS is enabled for simplicity
422        }
423    }
424
425    #[cfg(not(target_arch = "wasm32"))]
426    {
427        PlatformContext::Native {
428            os_info: Some(format!(
429                "{} {}",
430                std::env::consts::OS,
431                std::env::consts::ARCH
432            )),
433            system_resources: None, // Could be enhanced with system info
434        }
435    }
436}
437
438/// Detect available Web APIs in WASM environment
439#[cfg(target_arch = "wasm32")]
440#[allow(dead_code)]
441fn detect_available_web_apis() -> Vec<String> {
442    let mut apis = Vec::new();
443
444    if let Some(window) = web_sys::window() {
445        // Check for common Web APIs
446        if window.local_storage().is_ok() {
447            apis.push("localStorage".to_string());
448        }
449        if window.session_storage().is_ok() {
450            apis.push("sessionStorage".to_string());
451        }
452        apis.push("fetch".to_string()); // Fetch API is generally available
453    }
454
455    apis
456}
457
458#[cfg(not(target_arch = "wasm32"))]
459#[allow(dead_code)]
460fn detect_available_web_apis() -> Vec<String> {
461    Vec::new()
462}
463
464impl From<reqwest::Error> for Error {
465    fn from(err: reqwest::Error) -> Self {
466        let mut context = ErrorContext::default();
467
468        // Add HTTP context if available
469        if let Some(status) = err.status() {
470            context.http = Some(HttpErrorContext {
471                status_code: Some(status.as_u16()),
472                headers: None,
473                response_body: None,
474                url: err.url().map(|u| u.to_string()),
475                method: None,
476            });
477
478            // Determine if error is retryable
479            let retryable = match status.as_u16() {
480                500..=599 | 429 | 408 => true, // Server errors, rate limit, timeout
481                _ => false,
482            };
483
484            context.retry = Some(RetryInfo {
485                attempts: 0,
486                retryable,
487                retry_after: None,
488            });
489        }
490
491        // Add platform context
492        context.platform = Some(detect_platform_context());
493
494        Error::Http {
495            message: err.to_string(),
496            source: Some(err),
497            context,
498        }
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    #[test]
507    fn test_error_creation() {
508        let error = Error::auth("test message");
509        assert_eq!(error.to_string(), "Authentication error: test message");
510    }
511
512    #[test]
513    fn test_database_error() {
514        let error = Error::database("query failed");
515        assert_eq!(error.to_string(), "Database error: query failed");
516    }
517
518    #[test]
519    fn test_error_context() {
520        let error = Error::auth("test message");
521        assert!(error.context().is_some());
522        if let Some(context) = error.context() {
523            assert!(context.timestamp <= chrono::Utc::now());
524        }
525    }
526}