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 ChunkTooSmall {
75 chunk_length: u64,
77 upload_granularity: u64,
79 },
80 AtCapacity,
82 Unsupported,
84 BackendFailure,
86 BackendRateLimited,
88 BackendTimeout,
90 BackendUnavailable,
92 Panic,
94 UnexpectedTombstone,
96 CorruptData,
98 Internal,
100}
101
102impl fmt::Display for ErrorKind {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::InvalidMetadata => f.write_str("invalid object metadata"),
106 Self::InvalidUploadId => f.write_str("invalid upload id"),
107 Self::ClientStream => f.write_str("invalid client stream"),
108 Self::RangeNotSatisfiable { total } => {
109 write!(f, "range not satisfiable (object size: {total} bytes)")
110 }
111 Self::UploadOffsetMismatch { offset } => {
112 write!(f, "upload offset mismatch (server holds {offset} bytes)")
113 }
114 Self::UploadSessionGone => f.write_str("upload session gone"),
115 Self::UnknownUploadSession => f.write_str("unknown upload session"),
116 Self::ChunkExceedsUploadLength {
117 offset,
118 content_length,
119 upload_length,
120 } => write!(
121 f,
122 "chunk at offset {offset} with length {content_length} exceeds upload length {upload_length}"
123 ),
124 Self::ChunkTooSmall {
125 chunk_length,
126 upload_granularity,
127 } => write!(
128 f,
129 "non-final chunk length {chunk_length} is smaller than upload granularity {upload_granularity}"
130 ),
131 Self::AtCapacity => f.write_str("service at capacity"),
132 Self::Unsupported => f.write_str("unsupported operation"),
133 Self::BackendFailure => f.write_str("backend operation failed"),
134 Self::BackendRateLimited => f.write_str("backend rate limited"),
135 Self::BackendTimeout => f.write_str("backend timed out"),
136 Self::BackendUnavailable => f.write_str("backend unavailable"),
137 Self::CorruptData => f.write_str("corrupt stored data"),
138 Self::Panic => f.write_str("service task panicked"),
139 Self::UnexpectedTombstone => f.write_str("unexpected tombstone"),
140 Self::Internal => f.write_str("internal service error"),
141 }
142 }
143}
144
145pub struct Error {
150 kind: ErrorKind,
151 context: Option<Cow<'static, str>>,
152 source: Option<Box<dyn StdError + Send + Sync>>,
153}
154
155impl Error {
156 pub fn kind(&self) -> ErrorKind {
158 self.kind
159 }
160
161 pub fn new(kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Self {
163 Self::build(kind, Some(context.into()), None)
164 }
165
166 pub fn with_source<E>(kind: ErrorKind, source: E) -> Self
168 where
169 E: StdError + Send + Sync + 'static,
170 {
171 Self::build(kind, None, Some(Box::new(source)))
172 }
173
174 pub(crate) fn with_context<E>(
175 kind: ErrorKind,
176 context: impl Into<Cow<'static, str>>,
177 source: E,
178 ) -> Self
179 where
180 E: StdError + Send + Sync + 'static,
181 {
182 Self::build(kind, Some(context.into()), Some(Box::new(source)))
183 }
184
185 fn build(
186 kind: ErrorKind,
187 context: Option<Cow<'static, str>>,
188 source: Option<Box<dyn StdError + Send + Sync>>,
189 ) -> Self {
190 Self {
191 kind,
192 context,
193 source,
194 }
195 }
196
197 pub fn level(&self) -> Level {
199 match self.kind {
200 ErrorKind::InvalidMetadata => Level::DEBUG,
202 ErrorKind::InvalidUploadId => Level::DEBUG,
203 ErrorKind::ClientStream => Level::DEBUG,
204 ErrorKind::RangeNotSatisfiable { .. } => Level::DEBUG,
205 ErrorKind::UploadOffsetMismatch { .. } => Level::DEBUG,
206 ErrorKind::UploadSessionGone => Level::DEBUG,
207 ErrorKind::UnknownUploadSession => Level::DEBUG,
208 ErrorKind::ChunkExceedsUploadLength { .. } => Level::DEBUG,
209 ErrorKind::ChunkTooSmall { .. } => Level::DEBUG,
210 ErrorKind::Unsupported => Level::INFO,
214 ErrorKind::AtCapacity => Level::WARN,
216 ErrorKind::BackendRateLimited => Level::WARN,
217 ErrorKind::BackendTimeout => Level::WARN,
218 ErrorKind::BackendUnavailable => Level::WARN,
219 ErrorKind::BackendFailure => Level::ERROR,
221 ErrorKind::Panic => Level::ERROR,
222 ErrorKind::UnexpectedTombstone => Level::ERROR,
223 ErrorKind::CorruptData => Level::ERROR,
224 ErrorKind::Internal => Level::ERROR,
225 }
226 }
227}
228
229impl fmt::Display for Error {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 self.kind.fmt(f)?;
232 if let Some(context) = &self.context {
233 write!(f, ": {context}")?;
234 }
235 Ok(())
236 }
237}
238
239impl fmt::Debug for Error {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 f.debug_struct("Error")
242 .field("kind", &self.kind)
243 .field("context", &self.context)
244 .field("source", &self.source)
245 .finish()
246 }
247}
248
249impl StdError for Error {
250 fn source(&self) -> Option<&(dyn StdError + 'static)> {
251 self.source.as_deref().map(|source| source as _)
252 }
253}
254
255impl From<ErrorKind> for Error {
256 fn from(kind: ErrorKind) -> Self {
257 Self::build(kind, None, None)
258 }
259}
260
261impl From<Panic> for Error {
262 fn from(source: Panic) -> Self {
263 Self::with_source(ErrorKind::Panic, source)
264 }
265}
266
267pub trait ResultExt<T> {
269 fn context(self, kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Result<T>;
286
287 fn kind(self, kind: ErrorKind) -> Result<T>;
299}
300
301impl<T, E> ResultExt<T> for std::result::Result<T, E>
302where
303 E: StdError + Send + Sync + 'static,
304{
305 fn context(self, kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Result<T> {
306 self.map_err(|source| Error::with_context(kind, context, source))
307 }
308
309 fn kind(self, kind: ErrorKind) -> Result<T> {
310 self.map_err(|source| Error::with_source(kind, source))
311 }
312}
313
314impl From<std::io::Error> for Error {
315 fn from(source: std::io::Error) -> Self {
316 Self::with_source(ErrorKind::BackendFailure, source)
317 }
318}
319
320impl From<crate::stream::ClientError> for Error {
321 fn from(source: crate::stream::ClientError) -> Self {
322 Self::with_source(ErrorKind::ClientStream, source)
323 }
324}
325
326impl From<objectstore_types::multipart::InvalidUploadId> for Error {
327 fn from(source: objectstore_types::multipart::InvalidUploadId) -> Self {
328 Self::with_source(ErrorKind::InvalidUploadId, source)
329 }
330}
331
332pub type Result<T, E = Error> = std::result::Result<T, E>;
334
335#[cfg(test)]
336mod tests {
337 use std::error::Error as _;
338 use std::io;
339
340 use super::{Error, ErrorKind, Panic};
341
342 #[test]
343 fn opaque_error_preserves_source() {
344 let error = Error::with_source(ErrorKind::BackendFailure, io::Error::other("backend down"));
345 let standard_error: &dyn std::error::Error = &error;
346
347 assert_eq!(error.kind(), ErrorKind::BackendFailure);
348 assert_eq!(standard_error.source().unwrap().to_string(), "backend down");
349 }
350
351 #[test]
352 fn context_renders_after_kind() {
353 let error = Error::with_context(
354 ErrorKind::BackendFailure,
355 "reading local object",
356 io::Error::other("backend down"),
357 );
358
359 assert_eq!(
360 error.to_string(),
361 "backend operation failed: reading local object"
362 );
363 }
364
365 #[test]
366 fn error_kind_default_message_includes_range_size() {
367 let error: Error = ErrorKind::RangeNotSatisfiable { total: 42 }.into();
368
369 assert_eq!(
370 error.to_string(),
371 "range not satisfiable (object size: 42 bytes)"
372 );
373 }
374
375 #[test]
376 fn panic_uses_the_payload_message() {
377 let panic = Panic::new(Box::new("task panicked"));
378 let error: Error = panic.into();
379
380 assert_eq!(error.kind(), ErrorKind::Panic);
381 assert_eq!(error.to_string(), "service task panicked");
382 assert_eq!(error.source().unwrap().to_string(), "task panicked");
383 }
384}