wowlab_supabase/
errors.rs1const 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#[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 #[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 #[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 #[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 #[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 #[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}