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(all(target_arch = "wasm32", feature = "wasm"))]
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(all(target_arch = "wasm32", not(feature = "wasm")))]
426    {
427        PlatformContext::Wasm {
428            user_agent: None,
429            available_apis: Vec::new(),
430            cors_enabled: true,
431        }
432    }
433
434    #[cfg(not(target_arch = "wasm32"))]
435    {
436        PlatformContext::Native {
437            os_info: Some(format!(
438                "{} {}",
439                std::env::consts::OS,
440                std::env::consts::ARCH
441            )),
442            system_resources: None, // Could be enhanced with system info
443        }
444    }
445}
446
447/// Detect available Web APIs in WASM environment
448#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
449#[allow(dead_code)]
450fn detect_available_web_apis() -> Vec<String> {
451    let mut apis = Vec::new();
452
453    if let Some(window) = web_sys::window() {
454        // Check for common Web APIs
455        if window.local_storage().is_ok() {
456            apis.push("localStorage".to_string());
457        }
458        if window.session_storage().is_ok() {
459            apis.push("sessionStorage".to_string());
460        }
461        apis.push("fetch".to_string()); // Fetch API is generally available
462    }
463
464    apis
465}
466
467#[cfg(not(target_arch = "wasm32"))]
468#[allow(dead_code)]
469fn detect_available_web_apis() -> Vec<String> {
470    Vec::new()
471}
472
473impl From<reqwest::Error> for Error {
474    fn from(err: reqwest::Error) -> Self {
475        let mut context = ErrorContext::default();
476
477        // Add HTTP context if available
478        if let Some(status) = err.status() {
479            context.http = Some(HttpErrorContext {
480                status_code: Some(status.as_u16()),
481                headers: None,
482                response_body: None,
483                url: err.url().map(|u| u.to_string()),
484                method: None,
485            });
486
487            // Determine if error is retryable
488            let retryable = match status.as_u16() {
489                500..=599 | 429 | 408 => true, // Server errors, rate limit, timeout
490                _ => false,
491            };
492
493            context.retry = Some(RetryInfo {
494                attempts: 0,
495                retryable,
496                retry_after: None,
497            });
498        }
499
500        // Add platform context
501        context.platform = Some(detect_platform_context());
502
503        Error::Http {
504            message: err.to_string(),
505            source: Some(err),
506            context,
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    #[test]
516    fn test_error_creation() {
517        let error = Error::auth("test message");
518        assert_eq!(error.to_string(), "Authentication error: test message");
519    }
520
521    #[test]
522    fn test_database_error() {
523        let error = Error::database("query failed");
524        assert_eq!(error.to_string(), "Database error: query failed");
525    }
526
527    #[test]
528    fn test_error_context() {
529        let error = Error::auth("test message");
530        assert!(error.context().is_some());
531        if let Some(context) = error.context() {
532            assert!(context.timestamp <= chrono::Utc::now());
533        }
534    }
535}