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 disable_backtrace(&self) -> bool {
102        matches!(self, ErrorKind::NotFound)
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    source: Option<anyhow::Error>,
231    backtrace: Backtrace,
232}
233
234impl Display for Error {
235    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
236        write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
237
238        if !self.context.is_empty() {
239            write!(f, ", context: {{ ")?;
240            write!(
241                f,
242                "{}",
243                self.context
244                    .iter()
245                    .map(|(k, v)| format!("{k}: {v}"))
246                    .collect::<Vec<_>>()
247                    .join(", ")
248            )?;
249            write!(f, " }}")?;
250        }
251
252        if !self.message.is_empty() {
253            write!(f, " => {}", self.message)?;
254        }
255
256        if let Some(source) = &self.source {
257            write!(f, ", source: {source}")?;
258        }
259
260        Ok(())
261    }
262}
263
264impl Debug for Error {
265    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
266        // If alternate has been specified, we will print like Debug.
267        if f.alternate() {
268            let mut de = f.debug_struct("Error");
269            de.field("kind", &self.kind);
270            de.field("message", &self.message);
271            de.field("status", &self.status);
272            de.field("operation", &self.operation);
273            de.field("context", &self.context);
274            de.field("source", &self.source);
275            return de.finish();
276        }
277
278        write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
279        if !self.message.is_empty() {
280            write!(f, " => {}", self.message)?;
281        }
282        writeln!(f)?;
283
284        if !self.context.is_empty() {
285            writeln!(f)?;
286            writeln!(f, "Context:")?;
287            for (k, v) in self.context.iter() {
288                writeln!(f, "   {k}: {v}")?;
289            }
290        }
291        if let Some(source) = &self.source {
292            writeln!(f)?;
293            writeln!(f, "Source:")?;
294            writeln!(f, "   {source:#}")?;
295        }
296        if self.backtrace.status() == BacktraceStatus::Captured {
297            writeln!(f)?;
298            writeln!(f, "Backtrace:")?;
299            writeln!(f, "{}", self.backtrace)?;
300        }
301
302        Ok(())
303    }
304}
305
306impl std::error::Error for Error {
307    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
308        self.source.as_ref().map(|v| v.as_ref())
309    }
310}
311
312impl Error {
313    /// Create a new Error with error kind and message.
314    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
315        Self {
316            kind,
317            message: message.into(),
318
319            status: ErrorStatus::Permanent,
320            operation: "",
321            context: Vec::default(),
322            source: None,
323            // `Backtrace::capture()` will check if backtrace has been enabled
324            // internally. It's zero cost if backtrace is disabled.
325            backtrace: if kind.disable_backtrace() {
326                Backtrace::disabled()
327            } else {
328                Backtrace::capture()
329            },
330        }
331    }
332
333    /// Update error's operation.
334    ///
335    /// # Notes
336    ///
337    /// If the error already carries an operation, we will push a new context
338    /// `(called, operation)`.
339    pub fn with_operation(mut self, operation: impl Into<&'static str>) -> Self {
340        if !self.operation.is_empty() {
341            self.context.push(("called", self.operation.to_string()));
342        }
343
344        self.operation = operation.into();
345        self
346    }
347
348    /// Add more context in error.
349    pub fn with_context(mut self, key: &'static str, value: impl ToString) -> Self {
350        self.context.push((key, value.to_string()));
351        self
352    }
353
354    /// Set source for error.
355    ///
356    /// # Notes
357    ///
358    /// If the source has been set, we will raise a panic here.
359    pub fn set_source(mut self, src: impl Into<anyhow::Error>) -> Self {
360        debug_assert!(self.source.is_none(), "the source error has been set");
361
362        self.source = Some(src.into());
363        self
364    }
365
366    /// Operate on error with map.
367    pub fn map<F>(self, f: F) -> Self
368    where
369        F: FnOnce(Self) -> Self,
370    {
371        f(self)
372    }
373
374    /// Set permanent status for error.
375    pub fn set_permanent(mut self) -> Self {
376        self.status = ErrorStatus::Permanent;
377        self
378    }
379
380    /// Set temporary status for error.
381    ///
382    /// By set temporary, we indicate this error is retryable.
383    pub fn set_temporary(mut self) -> Self {
384        self.status = ErrorStatus::Temporary;
385        self
386    }
387
388    /// Set temporary status for error by given temporary.
389    ///
390    /// By set temporary, we indicate this error is retryable.
391    pub(crate) fn with_temporary(mut self, temporary: bool) -> Self {
392        if temporary {
393            self.status = ErrorStatus::Temporary;
394        }
395        self
396    }
397
398    /// Set persistent status for error.
399    ///
400    /// By setting persistent, we indicate the retry should be stopped.
401    pub fn set_persistent(mut self) -> Self {
402        self.status = ErrorStatus::Persistent;
403        self
404    }
405
406    /// Return error's kind.
407    pub fn kind(&self) -> ErrorKind {
408        self.kind
409    }
410
411    /// Check if this error is temporary.
412    pub fn is_temporary(&self) -> bool {
413        self.status == ErrorStatus::Temporary
414    }
415}
416
417impl From<Error> for io::Error {
418    fn from(err: Error) -> Self {
419        let kind = match err.kind() {
420            ErrorKind::NotFound => io::ErrorKind::NotFound,
421            ErrorKind::PermissionDenied => io::ErrorKind::PermissionDenied,
422            _ => io::ErrorKind::Other,
423        };
424
425        io::Error::new(kind, err)
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use anyhow::anyhow;
432    use pretty_assertions::assert_eq;
433    use std::sync::LazyLock;
434
435    use super::*;
436
437    static TEST_ERROR: LazyLock<Error> = LazyLock::new(|| Error {
438        kind: ErrorKind::Unexpected,
439        message: "something wrong happened".to_string(),
440        status: ErrorStatus::Permanent,
441        operation: "Read",
442        context: vec![
443            ("path", "/path/to/file".to_string()),
444            ("called", "send_async".to_string()),
445        ],
446        source: Some(anyhow!("networking error")),
447        backtrace: Backtrace::disabled(),
448    });
449
450    #[test]
451    fn test_error_display() {
452        let s = format!("{}", LazyLock::force(&TEST_ERROR));
453        assert_eq!(
454            s,
455            r#"Unexpected (permanent) at Read, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"#
456        );
457        println!("{:#?}", LazyLock::force(&TEST_ERROR));
458    }
459
460    #[test]
461    fn test_error_debug() {
462        let s = format!("{:?}", LazyLock::force(&TEST_ERROR));
463        assert_eq!(
464            s,
465            r#"Unexpected (permanent) at Read => something wrong happened
466
467Context:
468   path: /path/to/file
469   called: send_async
470
471Source:
472   networking error
473"#
474        )
475    }
476}