opendal/types/
error.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Errors that returned by OpenDAL
19//!
20//! # Examples
21//!
22//! ```
23//! # use anyhow::Result;
24//! # use opendal::EntryMode;
25//! # use opendal::Operator;
26//! use opendal::ErrorKind;
27//! # async fn test(op: Operator) -> Result<()> {
28//! if let Err(e) = op.stat("test_file").await {
29//!     if e.kind() == ErrorKind::NotFound {
30//!         println!("entry not exist")
31//!     }
32//! }
33//! # Ok(())
34//! # }
35//! ```
36
37use std::backtrace::Backtrace;
38use std::backtrace::BacktraceStatus;
39use std::fmt;
40use std::fmt::Debug;
41use std::fmt::Display;
42use std::fmt::Formatter;
43use std::io;
44
45/// Result that is a wrapper of `Result<T, opendal::Error>`
46pub type Result<T, E = Error> = std::result::Result<T, E>;
47
48/// ErrorKind is all kinds of Error of opendal.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum ErrorKind {
52    /// OpenDAL don't know what happened here, and no actions other than just
53    /// returning it back. For example, s3 returns an internal service error.
54    Unexpected,
55    /// Underlying service doesn't support this operation.
56    Unsupported,
57
58    /// The config for backend is invalid.
59    ConfigInvalid,
60    /// The given path is not found.
61    NotFound,
62    /// The given path doesn't have enough permission for this operation
63    PermissionDenied,
64    /// The given path is a directory.
65    IsADirectory,
66    /// The given path is not a directory.
67    NotADirectory,
68    /// The given path already exists thus we failed to the specified operation on it.
69    AlreadyExists,
70    /// Requests that sent to this path is over the limit, please slow down.
71    RateLimited,
72    /// The given file paths are same.
73    IsSameFile,
74    /// The condition of this operation is not match.
75    ///
76    /// The `condition` itself is context based.
77    ///
78    /// For example, in S3, the `condition` can be:
79    /// 1. writing a file with If-Match header but the file's ETag is not match (will get a 412 Precondition Failed).
80    /// 2. reading a file with If-None-Match header but the file's ETag is match (will get a 304 Not Modified).
81    ///
82    /// As OpenDAL cannot handle the `condition not match` error, it will always return this error to users.
83    /// So users could to handle this error by themselves.
84    ConditionNotMatch,
85    /// The range of the content is not satisfied.
86    ///
87    /// OpenDAL returns this error to indicate that the range of the read request is not satisfied.
88    RangeNotSatisfied,
89}
90
91impl ErrorKind {
92    /// Convert self into static str.
93    pub fn into_static(self) -> &'static str {
94        self.into()
95    }
96
97    /// Capturing a backtrace can be a quite expensive runtime operation.
98    /// For some kinds of errors, backtrace is not useful and we can skip it (e.g., check if a file exists).
99    ///
100    /// See <https://github.com/apache/opendal/discussions/5569>
101    fn enable_backtrace(&self) -> bool {
102        matches!(self, ErrorKind::Unexpected)
103    }
104}
105
106impl Display for ErrorKind {
107    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
108        write!(f, "{}", self.into_static())
109    }
110}
111
112impl From<ErrorKind> for &'static str {
113    fn from(v: ErrorKind) -> &'static str {
114        match v {
115            ErrorKind::Unexpected => "Unexpected",
116            ErrorKind::Unsupported => "Unsupported",
117            ErrorKind::ConfigInvalid => "ConfigInvalid",
118            ErrorKind::NotFound => "NotFound",
119            ErrorKind::PermissionDenied => "PermissionDenied",
120            ErrorKind::IsADirectory => "IsADirectory",
121            ErrorKind::NotADirectory => "NotADirectory",
122            ErrorKind::AlreadyExists => "AlreadyExists",
123            ErrorKind::RateLimited => "RateLimited",
124            ErrorKind::IsSameFile => "IsSameFile",
125            ErrorKind::ConditionNotMatch => "ConditionNotMatch",
126            ErrorKind::RangeNotSatisfied => "RangeNotSatisfied",
127        }
128    }
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132enum ErrorStatus {
133    /// Permanent means without external changes, the error never changes.
134    ///
135    /// For example, underlying services returns a not found error.
136    ///
137    /// Users SHOULD never retry this operation.
138    Permanent,
139    /// Temporary means this error is returned for temporary.
140    ///
141    /// For example, underlying services is rate limited or unavailable for temporary.
142    ///
143    /// Users CAN retry the operation to resolve it.
144    Temporary,
145    /// Persistent means this error used to be temporary but still failed after retry.
146    ///
147    /// For example, underlying services kept returning network errors.
148    ///
149    /// Users MAY retry this operation but it's highly possible to error again.
150    Persistent,
151}
152
153impl Display for ErrorStatus {
154    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
155        match self {
156            ErrorStatus::Permanent => write!(f, "permanent"),
157            ErrorStatus::Temporary => write!(f, "temporary"),
158            ErrorStatus::Persistent => write!(f, "persistent"),
159        }
160    }
161}
162
163/// Error is the error struct returned by all opendal functions.
164///
165/// ## Display
166///
167/// Error can be displayed in two ways:
168///
169/// - Via `Display`: like `err.to_string()` or `format!("{err}")`
170///
171/// Error will be printed in a single line:
172///
173/// ```shell
174/// Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"
175/// ```
176///
177/// - Via `Debug`: like `format!("{err:?}")`
178///
179/// Error will be printed in multi lines with more details and backtraces (if captured):
180///
181/// ```shell
182/// Unexpected => something wrong happened
183///
184/// Context:
185///    path: /path/to/file
186///    called: send_async
187///
188/// Source: networking error
189///
190/// Backtrace:
191///    0: opendal::error::Error::new
192///              at ./src/error.rs:197:24
193///    1: opendal::error::tests::generate_error
194///              at ./src/error.rs:241:9
195///    2: opendal::error::tests::test_error_debug_with_backtrace::{{closure}}
196///              at ./src/error.rs:305:41
197///    ...
198/// ```
199///
200/// - For conventional struct-style Debug representation, like `format!("{err:#?}")`:
201///
202/// ```shell
203/// Error {
204///     kind: Unexpected,
205///     message: "something wrong happened",
206///     status: Permanent,
207///     operation: "Read",
208///     context: [
209///         (
210///             "path",
211///             "/path/to/file",
212///         ),
213///         (
214///             "called",
215///             "send_async",
216///         ),
217///     ],
218///     source: Some(
219///         "networking error",
220///     ),
221/// }
222/// ```
223pub struct Error {
224    kind: ErrorKind,
225    message: String,
226
227    status: ErrorStatus,
228    operation: &'static str,
229    context: Vec<(&'static str, String)>,
230
231    source: Option<anyhow::Error>,
232    backtrace: Option<Box<Backtrace>>,
233}
234
235impl Display for Error {
236    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
237        write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
238
239        if !self.context.is_empty() {
240            write!(f, ", context: {{ ")?;
241            write!(
242                f,
243                "{}",
244                self.context
245                    .iter()
246                    .map(|(k, v)| format!("{k}: {v}"))
247                    .collect::<Vec<_>>()
248                    .join(", ")
249            )?;
250            write!(f, " }}")?;
251        }
252
253        if !self.message.is_empty() {
254            write!(f, " => {}", self.message)?;
255        }
256
257        if let Some(source) = &self.source {
258            write!(f, ", source: {source}")?;
259        }
260
261        Ok(())
262    }
263}
264
265impl Debug for Error {
266    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
267        // If alternate has been specified, we will print like Debug.
268        if f.alternate() {
269            let mut de = f.debug_struct("Error");
270            de.field("kind", &self.kind);
271            de.field("message", &self.message);
272            de.field("status", &self.status);
273            de.field("operation", &self.operation);
274            de.field("context", &self.context);
275            de.field("source", &self.source);
276            return de.finish();
277        }
278
279        write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
280        if !self.message.is_empty() {
281            write!(f, " => {}", self.message)?;
282        }
283        writeln!(f)?;
284
285        if !self.context.is_empty() {
286            writeln!(f)?;
287            writeln!(f, "Context:")?;
288            for (k, v) in self.context.iter() {
289                writeln!(f, "   {k}: {v}")?;
290            }
291        }
292        if let Some(source) = &self.source {
293            writeln!(f)?;
294            writeln!(f, "Source:")?;
295            writeln!(f, "   {source:#}")?;
296        }
297
298        if let Some(backtrace) = &self.backtrace {
299            writeln!(f)?;
300            writeln!(f, "Backtrace:")?;
301            writeln!(f, "{}", backtrace)?;
302        }
303
304        Ok(())
305    }
306}
307
308impl std::error::Error for Error {
309    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
310        self.source.as_ref().map(|v| v.as_ref())
311    }
312}
313
314impl Error {
315    /// Create a new Error with error kind and message.
316    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
317        Self {
318            kind,
319            message: message.into(),
320
321            status: ErrorStatus::Permanent,
322            operation: "",
323            context: Vec::default(),
324            source: None,
325
326            backtrace: kind
327                .enable_backtrace()
328                // `Backtrace::capture()` will check if backtrace has
329                // been enabled internally. It's zero cost if backtrace
330                // is disabled.
331                .then(Backtrace::capture)
332                // We only keep captured backtrace to avoid an extra box.
333                .filter(|bt| bt.status() == BacktraceStatus::Captured)
334                .map(Box::new),
335        }
336    }
337
338    /// Update error's operation.
339    ///
340    /// # Notes
341    ///
342    /// If the error already carries an operation, we will push a new context
343    /// `(called, operation)`.
344    pub fn with_operation(mut self, operation: impl Into<&'static str>) -> Self {
345        if !self.operation.is_empty() {
346            self.context.push(("called", self.operation.to_string()));
347        }
348
349        self.operation = operation.into();
350        self
351    }
352
353    /// Add more context in error.
354    pub fn with_context(mut self, key: &'static str, value: impl ToString) -> Self {
355        self.context.push((key, value.to_string()));
356        self
357    }
358
359    /// Set source for error.
360    ///
361    /// # Notes
362    ///
363    /// If the source has been set, we will raise a panic here.
364    pub fn set_source(mut self, src: impl Into<anyhow::Error>) -> Self {
365        debug_assert!(self.source.is_none(), "the source error has been set");
366
367        self.source = Some(src.into());
368        self
369    }
370
371    /// Operate on error with map.
372    pub fn map<F>(self, f: F) -> Self
373    where
374        F: FnOnce(Self) -> Self,
375    {
376        f(self)
377    }
378
379    /// Set permanent status for error.
380    pub fn set_permanent(mut self) -> Self {
381        self.status = ErrorStatus::Permanent;
382        self
383    }
384
385    /// Set temporary status for error.
386    ///
387    /// By set temporary, we indicate this error is retryable.
388    pub fn set_temporary(mut self) -> Self {
389        self.status = ErrorStatus::Temporary;
390        self
391    }
392
393    /// Set temporary status for error by given temporary.
394    ///
395    /// By set temporary, we indicate this error is retryable.
396    pub(crate) fn with_temporary(mut self, temporary: bool) -> Self {
397        if temporary {
398            self.status = ErrorStatus::Temporary;
399        }
400        self
401    }
402
403    /// Set persistent status for error.
404    ///
405    /// By setting persistent, we indicate the retry should be stopped.
406    pub fn set_persistent(mut self) -> Self {
407        self.status = ErrorStatus::Persistent;
408        self
409    }
410
411    /// Return error's kind.
412    pub fn kind(&self) -> ErrorKind {
413        self.kind
414    }
415
416    /// Check if this error is temporary.
417    pub fn is_temporary(&self) -> bool {
418        self.status == ErrorStatus::Temporary
419    }
420
421    /// Return error's backtrace.
422    ///
423    /// Note: the standard way of exposing backtrace is the unstable feature [`error_generic_member_access`](https://github.com/rust-lang/rust/issues/99301).
424    /// We don't provide it as it requires nightly rust.
425    ///
426    /// If you just want to print error with backtrace, use `Debug`, like `format!("{err:?}")`.
427    ///
428    /// If you use nightly rust, and want to access `opendal::Error`'s backtrace in the standard way, you can
429    /// implement a newtype like this:
430    ///
431    /// ```ignore
432    /// // assume you already have `#![feature(error_generic_member_access)]` on the top of your crate
433    ///
434    /// #[derive(::std::fmt::Debug)]
435    /// pub struct OpendalError(opendal::Error);
436    ///
437    /// impl std::fmt::Display for OpendalError {
438    ///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439    ///         self.0.fmt(f)
440    ///     }
441    /// }
442    ///
443    /// impl std::error::Error for OpendalError {
444    ///     fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
445    ///         if let Some(bt) = self.0.backtrace() {
446    ///             request.provide_ref::<std::backtrace::Backtrace>(bt);
447    ///         }
448    ///     }
449    ///
450    ///     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
451    ///         self.0.source()
452    ///     }
453    /// }
454    /// ```
455    ///
456    /// Additionally, you can add a clippy lint to prevent usage of the original `opendal::Error` type.
457    /// ```toml
458    /// disallowed-types = [
459    ///     { path = "opendal::Error", reason = "Please use `my_crate::OpendalError` instead." },
460    /// ]
461    /// ```
462    pub fn backtrace(&self) -> Option<&Backtrace> {
463        self.backtrace.as_ref().map(|bt| bt.as_ref())
464    }
465}
466
467impl From<Error> for io::Error {
468    fn from(err: Error) -> Self {
469        let kind = match err.kind() {
470            ErrorKind::NotFound => io::ErrorKind::NotFound,
471            ErrorKind::PermissionDenied => io::ErrorKind::PermissionDenied,
472            _ => io::ErrorKind::Other,
473        };
474
475        io::Error::new(kind, err)
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use anyhow::anyhow;
483    use pretty_assertions::assert_eq;
484    use std::mem::size_of;
485    use std::sync::LazyLock;
486
487    static TEST_ERROR: LazyLock<Error> = LazyLock::new(|| Error {
488        kind: ErrorKind::Unexpected,
489        message: "something wrong happened".to_string(),
490        status: ErrorStatus::Permanent,
491        operation: "Read",
492        context: vec![
493            ("path", "/path/to/file".to_string()),
494            ("called", "send_async".to_string()),
495        ],
496        source: Some(anyhow!("networking error")),
497        backtrace: None,
498    });
499
500    /// This is not a real test case.
501    ///
502    /// We assert our public structs here to make sure we don't introduce
503    /// unexpected struct/enum size change.
504    #[cfg(target_pointer_width = "64")]
505    #[test]
506    fn assert_size() {
507        assert_eq!(88, size_of::<Error>());
508    }
509
510    #[test]
511    fn test_error_display() {
512        let s = format!("{}", LazyLock::force(&TEST_ERROR));
513        assert_eq!(
514            s,
515            r#"Unexpected (permanent) at Read, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"#
516        );
517        println!("{:#?}", LazyLock::force(&TEST_ERROR));
518    }
519
520    #[test]
521    fn test_error_debug() {
522        let s = format!("{:?}", LazyLock::force(&TEST_ERROR));
523        assert_eq!(
524            s,
525            r#"Unexpected (permanent) at Read => something wrong happened
526
527Context:
528   path: /path/to/file
529   called: send_async
530
531Source:
532   networking error
533"#
534        )
535    }
536}