Skip to main content

rp_supabase_storage/
error.rs

1use reqwest::StatusCode;
2use serde::{Deserialize, Deserializer};
3
4/// Errors returned by [`StorageClient`](crate::StorageClient) and [`Bucket`](crate::Bucket).
5#[derive(Debug, thiserror::Error)]
6pub enum StorageError {
7    /// The Storage API answered with a non-success HTTP status.
8    #[error("storage API error ({status}): {body}")]
9    Api {
10        /// HTTP status of the response.
11        status: StatusCode,
12        /// Decoded response body.
13        body: ApiErrorBody,
14    },
15    /// A bucket id or object path is not valid.
16    #[error("invalid storage path {path:?}: {reason}")]
17    InvalidPath {
18        /// The rejected input.
19        path: String,
20        /// Why the input was rejected.
21        reason: PathError,
22    },
23    /// The project URL cannot carry path segments (for example `mailto:`).
24    #[error("project URL cannot be a base URL")]
25    InvalidBaseUrl,
26    /// The Storage API returned a signed URL without a `?token=` query.
27    #[error("signed URL from the storage API has no token: {0:?}")]
28    MissingSignedToken(String),
29    /// The Storage API did not sign an object, for example because it does not exist or the
30    /// caller cannot read it.
31    #[error("storage API did not sign {path:?}: {message}")]
32    SignFailed {
33        /// The requested object path.
34        path: String,
35        /// The reason from the Storage API.
36        message: String,
37    },
38    /// A URL could not be parsed or joined.
39    #[error(transparent)]
40    Url(#[from] url::ParseError),
41    /// The HTTP transport failed.
42    #[error(transparent)]
43    Http(#[from] reqwest::Error),
44    /// A success response body is not the expected JSON.
45    #[error(transparent)]
46    Json(#[from] serde_json::Error),
47    /// A key, token, or option is not a valid HTTP header value.
48    #[error(transparent)]
49    InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
50}
51
52impl StorageError {
53    /// Status the Storage API reports for an [`StorageError::Api`] error, `None` for other
54    /// errors.
55    ///
56    /// The object routes answer most errors with HTTP `400` and put the real status in the
57    /// body's `statusCode`, for example `"404"` for a missing object and `"403"` when row level
58    /// security denies access. This returns the body status when it is a valid status code and
59    /// the HTTP status otherwise.
60    #[must_use]
61    pub fn api_status(&self) -> Option<StatusCode> {
62        let Self::Api { status, body } = self else {
63            return None;
64        };
65        let reported = match body {
66            ApiErrorBody::Storage(StorageErrorBody {
67                status_code: Some(code),
68                ..
69            }) => code.parse::<u16>().ok(),
70            ApiErrorBody::Storage(_) | ApiErrorBody::Raw(_) => None,
71        };
72        Some(
73            reported
74                .and_then(|code| StatusCode::from_u16(code).ok())
75                .unwrap_or(*status),
76        )
77    }
78
79    /// Machine-readable `code` of an [`StorageError::Api`] error body, `None` for other
80    /// errors and for bodies without a code.
81    ///
82    /// For example, an upload without upsert to an existing path fails with
83    /// [`StorageErrorCode::KeyAlreadyExists`] or [`StorageErrorCode::ResourceAlreadyExists`],
84    /// and a delete that row level security blocks fails with
85    /// [`StorageErrorCode::AccessDenied`].
86    #[must_use]
87    pub const fn code(&self) -> Option<&StorageErrorCode> {
88        let Self::Api {
89            body: ApiErrorBody::Storage(body),
90            ..
91        } = self
92        else {
93            return None;
94        };
95        body.code.as_ref()
96    }
97}
98
99/// Reason for [`StorageError::InvalidPath`].
100#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
101pub enum PathError {
102    /// The path is empty.
103    #[error("path is empty")]
104    Empty,
105    /// The path contains an empty segment (`a//b`, a leading or a trailing `/`).
106    #[error("path contains an empty segment")]
107    EmptySegment,
108    /// The path contains a `.` or `..` segment.
109    #[error("path contains a `.` or `..` segment")]
110    DotSegment,
111    /// The path contains a tab, carriage return, or line feed. URL parsing drops these
112    /// characters silently, which changes the target object.
113    #[error("path contains a tab, carriage return, or line feed")]
114    ControlCharacter,
115}
116
117/// Body of a failed Storage API response.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum ApiErrorBody {
120    /// The standard Storage error JSON.
121    Storage(StorageErrorBody),
122    /// Any other body, kept as text.
123    Raw(String),
124}
125
126impl ApiErrorBody {
127    /// Decodes the standard error JSON. Text, and JSON with none of its fields, stay `Raw`.
128    pub(crate) fn decode(text: String) -> Self {
129        match serde_json::from_str::<StorageErrorBody>(&text) {
130            Ok(body)
131                if body.status_code.is_some()
132                    || body.code.is_some()
133                    || body.error.is_some()
134                    || body.message.is_some() =>
135            {
136                Self::Storage(body)
137            }
138            Ok(_) | Err(_) => Self::Raw(text),
139        }
140    }
141}
142
143impl core::fmt::Display for ApiErrorBody {
144    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
145        match self {
146            Self::Storage(body) => {
147                let label = body
148                    .code
149                    .as_ref()
150                    .map(StorageErrorCode::as_str)
151                    .or(body.error.as_deref());
152                match (label, body.message.as_deref()) {
153                    (Some(label), Some(message)) => write!(formatter, "{label}: {message}"),
154                    (Some(text), None) | (None, Some(text)) => formatter.write_str(text),
155                    (None, None) => Ok(()),
156                }
157            }
158            Self::Raw(text) => formatter.write_str(text),
159        }
160    }
161}
162
163/// Standard Storage error JSON, for example
164/// `{"statusCode": "409", "code": "KeyAlreadyExists", "error": "Duplicate", "message": "..."}`.
165/// Every field is optional because some responses omit them.
166#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
167pub struct StorageErrorBody {
168    /// Status code reported in the body. The API sends it as a string; numbers are converted.
169    #[serde(rename = "statusCode", default, deserialize_with = "string_or_number")]
170    pub status_code: Option<String>,
171    /// Machine-readable error code, for example `KeyAlreadyExists`.
172    #[serde(default)]
173    pub code: Option<StorageErrorCode>,
174    /// Short error name, for example `not_found` or `Duplicate`.
175    #[serde(default)]
176    pub error: Option<String>,
177    /// Human-readable message.
178    #[serde(default)]
179    pub message: Option<String>,
180}
181
182fn string_or_number<'de, D: Deserializer<'de>>(
183    deserializer: D,
184) -> Result<Option<String>, D::Error> {
185    #[derive(Deserialize)]
186    #[serde(untagged)]
187    enum Code {
188        Text(String),
189        Number(u64),
190    }
191    Ok(
192        Option::<Code>::deserialize(deserializer)?.map(|code| match code {
193            Code::Text(text) => text,
194            Code::Number(number) => number.to_string(),
195        }),
196    )
197}
198
199/// Declares [`StorageErrorCode`] with one variant per wire value, plus `Other`.
200macro_rules! storage_error_codes {
201    ($($variant:ident => $wire:literal,)+) => {
202        /// Machine-readable `code` of a Storage API error, from the `ErrorCode` enum of
203        /// supabase/storage. A code this crate does not know is kept in `Other`.
204        #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
205        #[serde(from = "String")]
206        #[non_exhaustive]
207        pub enum StorageErrorCode {
208            $(
209                #[doc = concat!("`", $wire, "`")]
210                $variant,
211            )+
212            /// Any other code.
213            Other(String),
214        }
215
216        impl StorageErrorCode {
217            /// Wire value of the code.
218            #[must_use]
219            pub fn as_str(&self) -> &str {
220                match self {
221                    $(Self::$variant => $wire,)+
222                    Self::Other(code) => code,
223                }
224            }
225        }
226
227        impl From<String> for StorageErrorCode {
228            fn from(code: String) -> Self {
229                match code.as_str() {
230                    $($wire => Self::$variant,)+
231                    _ => Self::Other(code),
232                }
233            }
234        }
235    };
236}
237
238storage_error_codes! {
239    NoSuchBucket => "NoSuchBucket",
240    NoSuchKey => "NoSuchKey",
241    NoSuchUpload => "NoSuchUpload",
242    InvalidJwt => "InvalidJWT",
243    InvalidRequest => "InvalidRequest",
244    InvalidArgument => "InvalidArgument",
245    TenantNotFound => "TenantNotFound",
246    EntityTooLarge => "EntityTooLarge",
247    EntityTooSmall => "EntityTooSmall",
248    InternalError => "InternalError",
249    ResourceAlreadyExists => "ResourceAlreadyExists",
250    ResourceNotEmpty => "ResourceNotEmpty",
251    InvalidBucketName => "InvalidBucketName",
252    InvalidKey => "InvalidKey",
253    InvalidRange => "InvalidRange",
254    InvalidMimeType => "InvalidMimeType",
255    InvalidUploadId => "InvalidUploadId",
256    KeyAlreadyExists => "KeyAlreadyExists",
257    BucketAlreadyExists => "BucketAlreadyExists",
258    DatabaseTimeout => "DatabaseTimeout",
259    DatabaseReadOnly => "DatabaseReadOnly",
260    InvalidSignature => "InvalidSignature",
261    ExpiredToken => "ExpiredToken",
262    SignatureDoesNotMatch => "SignatureDoesNotMatch",
263    AccessDenied => "AccessDenied",
264    ResourceLocked => "ResourceLocked",
265    ResourceReferenced => "ResourceReferenced",
266    DatabaseError => "DatabaseError",
267    MissingContentLength => "MissingContentLength",
268    MissingParameter => "MissingParameter",
269    InvalidParameter => "InvalidParameter",
270    InvalidUploadSignature => "InvalidUploadSignature",
271    LockTimeout => "LockTimeout",
272    PreconditionFailed => "PreconditionFailed",
273    InvalidChecksum => "InvalidChecksum",
274    SlowDown => "SlowDown",
275    FeatureNotEnabled => "FeatureNotEnabled",
276    NotSupported => "NotSupported",
277    UnknownError => "UnknownError",
278}
279
280impl core::fmt::Display for StorageErrorCode {
281    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282        formatter.write_str(self.as_str())
283    }
284}