Skip to main content

objectstore_service/
error.rs

1//! Error types for service and backend operations.
2//!
3//! [`Error`] covers I/O, serialization, HTTP, metadata, authentication,
4//! and backend-specific failures. [`Result`] is the corresponding alias.
5
6use std::any::Any;
7use std::fmt;
8
9use objectstore_log::Level;
10use reqwest::StatusCode;
11use thiserror::Error as ThisError;
12
13use crate::stream::ClientError;
14
15/// Structured error detail parsed from a backend HTTP error response.
16///
17/// Formats conditionally: includes only the fields that are non-empty.
18#[derive(Debug)]
19pub struct BackendDetail {
20    /// Machine-readable error code (e.g., "InvalidArgument", "NoSuchKey").
21    pub code: String,
22    /// Human-readable error message from the response body.
23    pub message: String,
24}
25
26impl BackendDetail {
27    /// Creates a new [`BackendDetail`] with empty code and message.
28    pub fn none() -> Self {
29        Self {
30            code: String::new(),
31            message: String::new(),
32        }
33    }
34}
35
36impl fmt::Display for BackendDetail {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match (self.code.is_empty(), self.message.is_empty()) {
39            (false, false) => write!(f, "{} (backend code {})", self.message, self.code),
40            (true, false) => write!(f, "{}", self.message),
41            (false, true) => write!(f, "backend code {}", self.code),
42            (true, true) => Ok(()),
43        }
44    }
45}
46
47/// Error type for service operations.
48#[derive(Debug, ThisError)]
49pub enum Error {
50    /// IO errors related to payload streaming or file operations.
51    #[error("i/o error: {0}")]
52    Io(#[from] std::io::Error),
53
54    /// Error originating from a client-supplied input stream.
55    ///
56    /// Indicates the client is at fault (e.g. dropped connection mid-upload) and should
57    /// map to a 4xx response rather than a 5xx.
58    #[error("error reading client stream: {0}")]
59    Client(#[from] ClientError),
60
61    /// Errors related to de/serialization.
62    #[error("serde error: {context}")]
63    Serde {
64        /// Context describing what was being serialized/deserialized.
65        context: String,
66        /// The underlying serde error.
67        #[source]
68        cause: serde_json::Error,
69    },
70
71    /// All errors stemming from the reqwest client, used in multiple backends to send requests to
72    /// e.g. GCP APIs.
73    /// These can be network errors encountered when sending the requests, but can also indicate
74    /// errors returned by the API itself.
75    #[error("reqwest error: {context}")]
76    Reqwest {
77        /// Context describing the request that failed.
78        context: String,
79        /// The underlying reqwest error.
80        #[source]
81        cause: reqwest::Error,
82    },
83
84    /// An HTTP error response from a storage backend (e.g., GCS, S3).
85    ///
86    /// Unlike [`Reqwest`](Self::Reqwest), which covers transport-level failures, this variant
87    /// captures application-level error responses where the server returned a 4xx/5xx status code
88    /// along with a structured error body.
89    #[error("{context} ({status}). {detail}")]
90    BackendResponse {
91        /// Context describing the request that failed.
92        context: &'static str,
93        /// The HTTP status code returned by the backend.
94        status: StatusCode,
95        /// Parsed error code and message from the response body.
96        detail: BackendDetail,
97    },
98
99    /// Errors related to de/serialization and parsing of object metadata.
100    #[error("metadata error: {0}")]
101    Metadata(#[from] objectstore_types::metadata::Error),
102
103    /// Errors encountered when attempting to authenticate with GCP.
104    #[error("GCP authentication error: {0}")]
105    GcpAuth(#[from] gcp_auth::Error),
106
107    /// A spawned service task panicked.
108    #[error("service task failed: {0}")]
109    Panic(String),
110
111    /// A spawned service task was dropped before it could deliver its result.
112    ///
113    /// This is an unexpected condition that can occur when the runtime drops the task for unknown
114    /// reasons.
115    #[error("task dropped")]
116    Dropped,
117
118    /// A redirect tombstone was encountered at a place where it is not supported.
119    ///
120    /// This indicates a caller bug — tombstone-aware reads must go through the
121    /// [`HighVolumeBackend`](crate::backend::common::HighVolumeBackend) methods.
122    #[error("unexpected tombstone")]
123    UnexpectedTombstone,
124
125    /// The requested byte range is not satisfiable for the object's size.
126    #[error("range not satisfiable (object size: {total} bytes)")]
127    RangeNotSatisfiable {
128        /// Total size of the object in bytes.
129        total: u64,
130    },
131
132    /// The service has reached its concurrency limit and cannot accept more operations.
133    #[error("concurrency limit reached")]
134    AtCapacity,
135
136    /// Any other error stemming from one of the storage backends, which might be specific to that
137    /// backend or to a certain operation.
138    #[error("storage backend error: {context}")]
139    Generic {
140        /// Context describing the operation that failed.
141        context: String,
142        /// The underlying error, if available.
143        #[source]
144        cause: Option<Box<dyn std::error::Error + Send + Sync>>,
145    },
146
147    /// The functionality is not implemented by this instance of the service.
148    #[error("not implemented")]
149    NotImplemented,
150
151    /// Invalid upload ID (e.g. path traversal attempt).
152    #[error(transparent)]
153    InvalidUploadId(#[from] objectstore_types::multipart::InvalidUploadId),
154}
155
156impl Error {
157    /// Creates an [`Error::Panic`] from a panic payload, extracting the message.
158    pub fn panic(payload: Box<dyn Any + Send>) -> Self {
159        let msg = if let Some(s) = payload.downcast_ref::<&str>() {
160            (*s).to_owned()
161        } else if let Some(s) = payload.downcast_ref::<String>() {
162            s.clone()
163        } else {
164            "unknown panic".to_owned()
165        };
166        Self::Panic(msg)
167    }
168
169    /// Creates an [`Error::Reqwest`] from a reqwest error with context.
170    pub fn reqwest(context: impl Into<String>, cause: reqwest::Error) -> Self {
171        Self::Reqwest {
172            context: context.into(),
173            cause,
174        }
175    }
176
177    /// Creates an [`Error::Serde`] from a serde error with context.
178    pub fn serde(context: impl Into<String>, cause: serde_json::Error) -> Self {
179        Self::Serde {
180            context: context.into(),
181            cause,
182        }
183    }
184
185    /// Creates an [`Error::Generic`] with a context string and no cause.
186    pub fn generic(context: impl Into<String>) -> Self {
187        Self::Generic {
188            context: context.into(),
189            cause: None,
190        }
191    }
192
193    /// Returns the appropriate log level for this error.
194    pub fn level(&self) -> Level {
195        match self {
196            // Malformed client input at DEBUG level
197            Self::Client(_) => Level::DEBUG,
198            Self::Metadata(_) => Level::DEBUG,
199            Self::RangeNotSatisfiable { .. } => Level::DEBUG,
200            // Like rate limits, we treat capacity errors as warnings
201            Self::AtCapacity => Level::WARN,
202            // All other errors are service or backend failures
203            Self::Io(_) => Level::ERROR,
204            Self::Serde { .. } => Level::ERROR,
205            Self::Reqwest { .. } => Level::ERROR,
206            Self::BackendResponse { .. } => Level::ERROR,
207            Self::GcpAuth(_) => Level::ERROR,
208            Self::Panic(_) => Level::ERROR,
209            Self::Dropped => Level::ERROR,
210            Self::UnexpectedTombstone => Level::ERROR,
211            Self::NotImplemented => Level::ERROR,
212            Self::InvalidUploadId(_) => Level::DEBUG,
213            Self::Generic { .. } => Level::ERROR,
214        }
215    }
216}
217
218/// Result type for service operations.
219pub type Result<T, E = Error> = std::result::Result<T, E>;