1use std::any::Any;
8use std::borrow::Cow;
9use std::error::Error as StdError;
10use std::fmt;
11
12use objectstore_log::Level;
13#[derive(Debug)]
15pub struct Panic {
16 message: Cow<'static, str>,
17}
18
19impl Panic {
20 pub fn new(payload: Box<dyn Any + Send>) -> Self {
22 let message = if let Some(s) = payload.downcast_ref::<&str>() {
23 Cow::Borrowed(*s)
24 } else if let Some(s) = payload.downcast_ref::<String>() {
25 Cow::Owned(s.clone())
26 } else {
27 Cow::Borrowed("unknown panic")
28 };
29 Self { message }
30 }
31}
32
33impl fmt::Display for Panic {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.write_str(&self.message)
36 }
37}
38
39impl StdError for Panic {}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum ErrorKind {
44 InvalidMetadata,
46 InvalidUploadId,
48 ClientStream,
50 RangeNotSatisfiable {
52 total: u64,
54 },
55 UploadOffsetMismatch {
57 offset: u64,
59 },
60 UploadSessionGone,
62 UnknownUploadSession,
64 ChunkExceedsUploadLength {
66 offset: u64,
68 content_length: u64,
70 upload_length: u64,
72 },
73 AtCapacity,
75 Unsupported,
77 BackendFailure,
79 BackendRateLimited,
81 BackendTimeout,
83 BackendUnavailable,
85 Panic,
87 UnexpectedTombstone,
89 CorruptData,
91 Internal,
93}
94
95impl fmt::Display for ErrorKind {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 Self::InvalidMetadata => f.write_str("invalid object metadata"),
99 Self::InvalidUploadId => f.write_str("invalid upload id"),
100 Self::ClientStream => f.write_str("invalid client stream"),
101 Self::RangeNotSatisfiable { total } => {
102 write!(f, "range not satisfiable (object size: {total} bytes)")
103 }
104 Self::UploadOffsetMismatch { offset } => {
105 write!(f, "upload offset mismatch (server holds {offset} bytes)")
106 }
107 Self::UploadSessionGone => f.write_str("upload session gone"),
108 Self::UnknownUploadSession => f.write_str("unknown upload session"),
109 Self::ChunkExceedsUploadLength {
110 offset,
111 content_length,
112 upload_length,
113 } => write!(
114 f,
115 "chunk at offset {offset} with length {content_length} exceeds upload length {upload_length}"
116 ),
117 Self::AtCapacity => f.write_str("service at capacity"),
118 Self::Unsupported => f.write_str("unsupported operation"),
119 Self::BackendFailure => f.write_str("backend operation failed"),
120 Self::BackendRateLimited => f.write_str("backend rate limited"),
121 Self::BackendTimeout => f.write_str("backend timed out"),
122 Self::BackendUnavailable => f.write_str("backend unavailable"),
123 Self::CorruptData => f.write_str("corrupt stored data"),
124 Self::Panic => f.write_str("service task panicked"),
125 Self::UnexpectedTombstone => f.write_str("unexpected tombstone"),
126 Self::Internal => f.write_str("internal service error"),
127 }
128 }
129}
130
131pub struct Error {
136 kind: ErrorKind,
137 context: Option<Cow<'static, str>>,
138 source: Option<Box<dyn StdError + Send + Sync>>,
139}
140
141impl Error {
142 pub fn kind(&self) -> ErrorKind {
144 self.kind
145 }
146
147 pub fn new(kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Self {
149 Self::build(kind, Some(context.into()), None)
150 }
151
152 pub fn with_source<E>(kind: ErrorKind, source: E) -> Self
154 where
155 E: StdError + Send + Sync + 'static,
156 {
157 Self::build(kind, None, Some(Box::new(source)))
158 }
159
160 pub(crate) fn with_context<E>(
161 kind: ErrorKind,
162 context: impl Into<Cow<'static, str>>,
163 source: E,
164 ) -> Self
165 where
166 E: StdError + Send + Sync + 'static,
167 {
168 Self::build(kind, Some(context.into()), Some(Box::new(source)))
169 }
170
171 fn build(
172 kind: ErrorKind,
173 context: Option<Cow<'static, str>>,
174 source: Option<Box<dyn StdError + Send + Sync>>,
175 ) -> Self {
176 Self {
177 kind,
178 context,
179 source,
180 }
181 }
182
183 pub fn level(&self) -> Level {
185 match self.kind {
186 ErrorKind::InvalidMetadata => Level::DEBUG,
188 ErrorKind::InvalidUploadId => Level::DEBUG,
189 ErrorKind::ClientStream => Level::DEBUG,
190 ErrorKind::RangeNotSatisfiable { .. } => Level::DEBUG,
191 ErrorKind::UploadOffsetMismatch { .. } => Level::DEBUG,
192 ErrorKind::UploadSessionGone => Level::DEBUG,
193 ErrorKind::UnknownUploadSession => Level::DEBUG,
194 ErrorKind::ChunkExceedsUploadLength { .. } => Level::DEBUG,
195 ErrorKind::Unsupported => Level::INFO,
199 ErrorKind::AtCapacity => Level::WARN,
201 ErrorKind::BackendRateLimited => Level::WARN,
202 ErrorKind::BackendTimeout => Level::WARN,
203 ErrorKind::BackendUnavailable => Level::WARN,
204 ErrorKind::BackendFailure => Level::ERROR,
206 ErrorKind::Panic => Level::ERROR,
207 ErrorKind::UnexpectedTombstone => Level::ERROR,
208 ErrorKind::CorruptData => Level::ERROR,
209 ErrorKind::Internal => Level::ERROR,
210 }
211 }
212}
213
214impl fmt::Display for Error {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 self.kind.fmt(f)?;
217 if let Some(context) = &self.context {
218 write!(f, ": {context}")?;
219 }
220 Ok(())
221 }
222}
223
224impl fmt::Debug for Error {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 f.debug_struct("Error")
227 .field("kind", &self.kind)
228 .field("context", &self.context)
229 .field("source", &self.source)
230 .finish()
231 }
232}
233
234impl StdError for Error {
235 fn source(&self) -> Option<&(dyn StdError + 'static)> {
236 self.source.as_deref().map(|source| source as _)
237 }
238}
239
240impl From<ErrorKind> for Error {
241 fn from(kind: ErrorKind) -> Self {
242 Self::build(kind, None, None)
243 }
244}
245
246impl From<Panic> for Error {
247 fn from(source: Panic) -> Self {
248 Self::with_source(ErrorKind::Panic, source)
249 }
250}
251
252pub trait ResultExt<T> {
254 fn context(self, kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Result<T>;
271
272 fn kind(self, kind: ErrorKind) -> Result<T>;
284}
285
286impl<T, E> ResultExt<T> for std::result::Result<T, E>
287where
288 E: StdError + Send + Sync + 'static,
289{
290 fn context(self, kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Result<T> {
291 self.map_err(|source| Error::with_context(kind, context, source))
292 }
293
294 fn kind(self, kind: ErrorKind) -> Result<T> {
295 self.map_err(|source| Error::with_source(kind, source))
296 }
297}
298
299impl From<std::io::Error> for Error {
300 fn from(source: std::io::Error) -> Self {
301 Self::with_source(ErrorKind::BackendFailure, source)
302 }
303}
304
305impl From<crate::stream::ClientError> for Error {
306 fn from(source: crate::stream::ClientError) -> Self {
307 Self::with_source(ErrorKind::ClientStream, source)
308 }
309}
310
311impl From<objectstore_types::multipart::InvalidUploadId> for Error {
312 fn from(source: objectstore_types::multipart::InvalidUploadId) -> Self {
313 Self::with_source(ErrorKind::InvalidUploadId, source)
314 }
315}
316
317pub type Result<T, E = Error> = std::result::Result<T, E>;
319
320#[cfg(test)]
321mod tests {
322 use std::error::Error as _;
323 use std::io;
324
325 use super::{Error, ErrorKind, Panic};
326
327 #[test]
328 fn opaque_error_preserves_source() {
329 let error = Error::with_source(ErrorKind::BackendFailure, io::Error::other("backend down"));
330 let standard_error: &dyn std::error::Error = &error;
331
332 assert_eq!(error.kind(), ErrorKind::BackendFailure);
333 assert_eq!(standard_error.source().unwrap().to_string(), "backend down");
334 }
335
336 #[test]
337 fn context_renders_after_kind() {
338 let error = Error::with_context(
339 ErrorKind::BackendFailure,
340 "reading local object",
341 io::Error::other("backend down"),
342 );
343
344 assert_eq!(
345 error.to_string(),
346 "backend operation failed: reading local object"
347 );
348 }
349
350 #[test]
351 fn error_kind_default_message_includes_range_size() {
352 let error: Error = ErrorKind::RangeNotSatisfiable { total: 42 }.into();
353
354 assert_eq!(
355 error.to_string(),
356 "range not satisfiable (object size: 42 bytes)"
357 );
358 }
359
360 #[test]
361 fn panic_uses_the_payload_message() {
362 let panic = Panic::new(Box::new("task panicked"));
363 let error: Error = panic.into();
364
365 assert_eq!(error.kind(), ErrorKind::Panic);
366 assert_eq!(error.to_string(), "service task panicked");
367 assert_eq!(error.source().unwrap().to_string(), "task panicked");
368 }
369}