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}