Skip to main content

wowlab_supabase/
errors.rs

1const SERVER_ERROR_STATUS: u16 = 500;
2
3#[derive(Debug, thiserror::Error)]
4enum EnvironmentError {
5    #[error("variable is not present")]
6    NotPresent,
7    #[error("value is not valid Unicode")]
8    NotUnicode,
9}
10
11impl From<std::env::VarError> for EnvironmentError {
12    fn from(error: std::env::VarError) -> Self {
13        match error {
14            std::env::VarError::NotPresent => Self::NotPresent,
15            std::env::VarError::NotUnicode(_) => Self::NotUnicode,
16        }
17    }
18}
19
20/// Failure returned by the Supabase client.
21#[derive(Debug, thiserror::Error)]
22#[error(transparent)]
23pub struct SupabaseError {
24    kind: SupabaseErrorKind,
25}
26
27#[derive(Debug, thiserror::Error)]
28enum SupabaseErrorKind {
29    #[error("HTTP error: {source}")]
30    Http {
31        #[source]
32        source: reqwest::Error,
33    },
34    #[error("Parse error: {source}")]
35    Parse {
36        #[source]
37        source: serde_json::Error,
38    },
39    #[error("Response decode error for {url}: {source}, body: {body}")]
40    Decode {
41        url: String,
42        body: String,
43        #[source]
44        source: serde_json::Error,
45    },
46    #[error("Rate limited, retry after {retry_after_ms}ms")]
47    RateLimited { retry_after_ms: u64 },
48    #[error("Server error ({status}): {message}")]
49    Server { status: u16, message: String },
50    #[error("Unable to read environment variable '{name}': {source}")]
51    EnvVar {
52        name: String,
53        #[source]
54        source: EnvironmentError,
55    },
56    #[error("Failed to build HTTP client: {source}")]
57    ClientBuild {
58        #[source]
59        source: reqwest::Error,
60    },
61}
62
63impl SupabaseError {
64    /// Creates an HTTP-status error with its response body.
65    #[must_use]
66    pub fn server(status: u16, message: impl Into<String>) -> Self {
67        Self::new(SupabaseErrorKind::Server {
68            status,
69            message: message.into(),
70        })
71    }
72
73    pub(crate) fn decode(
74        url: impl Into<String>,
75        body: impl Into<String>,
76        source: serde_json::Error,
77    ) -> Self {
78        Self::new(SupabaseErrorKind::Decode {
79            url: url.into(),
80            body: body.into(),
81            source,
82        })
83    }
84
85    pub(crate) fn env_var(name: impl Into<String>, source: std::env::VarError) -> Self {
86        Self::new(SupabaseErrorKind::EnvVar {
87            name: name.into(),
88            source: source.into(),
89        })
90    }
91
92    pub(crate) fn client_build(source: reqwest::Error) -> Self {
93        Self::new(SupabaseErrorKind::ClientBuild { source })
94    }
95
96    pub(crate) fn rate_limited(retry_after_ms: u64) -> Self {
97        Self::new(SupabaseErrorKind::RateLimited { retry_after_ms })
98    }
99
100    fn new(kind: SupabaseErrorKind) -> Self {
101        Self { kind }
102    }
103
104    /// Returns the missing environment variable's name, when applicable.
105    #[must_use]
106    pub fn env_var_name(&self) -> Option<&str> {
107        match &self.kind {
108            SupabaseErrorKind::EnvVar { name, .. } => Some(name),
109            _ => None,
110        }
111    }
112
113    /// Returns the rejected HTTP status code, when applicable.
114    #[must_use]
115    pub fn server_status(&self) -> Option<u16> {
116        match self.kind {
117            SupabaseErrorKind::Server { status, .. } => Some(status),
118            _ => None,
119        }
120    }
121
122    /// Returns the server response body or the retained JSON-decode preview, when applicable.
123    #[must_use]
124    pub fn response_body(&self) -> Option<&str> {
125        match &self.kind {
126            SupabaseErrorKind::Decode { body, .. }
127            | SupabaseErrorKind::Server { message: body, .. } => Some(body),
128            _ => None,
129        }
130    }
131
132    /// Returns the server-requested retry delay in milliseconds, when rate limited.
133    #[must_use]
134    pub fn retry_after_ms(&self) -> Option<u64> {
135        match self.kind {
136            SupabaseErrorKind::RateLimited { retry_after_ms } => Some(retry_after_ms),
137            _ => None,
138        }
139    }
140
141    pub(crate) fn is_retryable(&self) -> bool {
142        matches!(
143            self.kind,
144            SupabaseErrorKind::Http { .. } | SupabaseErrorKind::RateLimited { .. }
145        ) || self
146            .server_status()
147            .is_some_and(|status| status >= SERVER_ERROR_STATUS)
148    }
149}
150
151impl From<reqwest::Error> for SupabaseError {
152    fn from(error: reqwest::Error) -> Self {
153        Self::new(SupabaseErrorKind::Http { source: error })
154    }
155}
156
157impl From<serde_json::Error> for SupabaseError {
158    fn from(error: serde_json::Error) -> Self {
159        Self::new(SupabaseErrorKind::Parse { source: error })
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::error::Error as _;
166
167    use googletest::prelude::*;
168
169    use super::SupabaseError;
170
171    #[gtest]
172    fn test_environment_error_preserves_name_and_typed_source() -> Result<()> {
173        let error = SupabaseError::env_var("SUPABASE_URL", std::env::VarError::NotPresent);
174
175        verify_that!(error.env_var_name(), some(eq("SUPABASE_URL")))?;
176        verify_true!(
177            error
178                .source()
179                .is_some_and(<dyn std::error::Error + 'static>::is::<super::EnvironmentError>)
180        )?;
181
182        Ok(())
183    }
184
185    #[gtest]
186    fn test_non_unicode_environment_error_redacts_value() -> Result<()> {
187        let secret = "service-role-secret";
188        let error = SupabaseError::env_var(
189            "SUPABASE_SERVICE_ROLE_KEY",
190            std::env::VarError::NotUnicode(secret.into()),
191        );
192
193        verify_that!(error.to_string(), not(contains_substring(secret)))?;
194        verify_that!(format!("{error:?}"), not(contains_substring(secret)))?;
195        verify_that!(
196            error.source().map(ToString::to_string).as_deref(),
197            some(eq("value is not valid Unicode"))
198        )?;
199
200        Ok(())
201    }
202
203    #[gtest]
204    fn test_json_conversion_preserves_typed_source() -> Result<()> {
205        let source = serde_json::from_str::<serde_json::Value>("{")
206            .err()
207            .or_fail()?;
208        let error = SupabaseError::from(source);
209
210        verify_true!(
211            error
212                .source()
213                .is_some_and(<dyn std::error::Error + 'static>::is::<serde_json::Error>)
214        )
215    }
216}