Skip to main content

krabka_object_store/
error.rs

1//! Error taxonomy for object-store construction and access.
2
3use {
4    super::object_store_api as object_store,
5    object_store::path::{Path as ObjectPath, Path},
6    std::io,
7};
8
9/// Errors raised by the object-store substrate.
10#[derive(Debug, thiserror::Error)]
11pub enum ObjectStoreError {
12    /// A local-filesystem I/O failure, for example during a read of a segment
13    /// file to upload.
14    #[error("object store I/O error: {0}")]
15    Io(#[from] io::Error),
16    /// The backend builder rejected the config: a bad bucket, region,
17    /// endpoint, or credential.
18    #[error("invalid object store config: {0}")]
19    InvalidConfig(String),
20    /// A specific object was not found. The variant is structured, so
21    /// consumers can upgrade it to their own domain error without string
22    /// matching.
23    #[error("object not found: {0}")]
24    NotFound(ObjectPath),
25    /// An object exceeded a caller-supplied size cap during a buffered read.
26    /// This guards against OOM on a corrupt or malicious oversized object.
27    #[error("object `{key}` is {size} bytes, exceeds cap of {max_bytes} bytes")]
28    TooLarge {
29        /// The object that breached the cap.
30        key: Path,
31        /// The metadata size or the bytes observed when streaming exceeded the cap.
32        size: u64,
33        /// The cap in bytes.
34        max_bytes: u64,
35    },
36    /// A conditional create (`PutMode::Create`) found an object already at
37    /// the key.
38    #[error("object already exists: {0}")]
39    AlreadyExists(ObjectPath),
40    /// A conditional read or write's precondition did not hold.
41    #[error("precondition failed for `{key}`: {reason}")]
42    Precondition {
43        /// The object whose precondition was evaluated.
44        key: ObjectPath,
45        /// Why the backend rejected the operation.
46        reason: String,
47    },
48    /// Any other backend error. The error becomes a string, so the public
49    /// surface stays stable.
50    #[error("object store backend error: {0}")]
51    Backend(String),
52}
53
54impl From<object_store::Error> for ObjectStoreError {
55    fn from(err: object_store::Error) -> Self {
56        match err {
57            object_store::Error::NotFound { path, .. } => Self::NotFound(ObjectPath::from(path)),
58            object_store::Error::AlreadyExists { path, .. } => {
59                Self::AlreadyExists(ObjectPath::from(path))
60            }
61            object_store::Error::Precondition { path, source } => Self::Precondition {
62                key: ObjectPath::from(path),
63                reason: source.to_string(),
64            },
65            other => Self::Backend(other.to_string()),
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use {
73        super::{object_store, *},
74        assert2::assert,
75        object_store::path::Path,
76        std::{io, io::ErrorKind},
77    };
78
79    #[test]
80    fn io_error_converts_via_from() {
81        let io = io::Error::new(ErrorKind::NotFound, "nope");
82        let err: ObjectStoreError = io.into();
83        assert!(matches!(err, ObjectStoreError::Io(_)));
84    }
85
86    #[test]
87    fn not_found_maps_to_structured_variant() {
88        let err = object_store::Error::NotFound {
89            path: "tenant/block".to_owned(),
90            source: "missing".into(),
91        };
92        let mapped = ObjectStoreError::from(err);
93        assert!(
94            matches!(&mapped, ObjectStoreError::NotFound(p) if p.to_string() == "tenant/block")
95        );
96    }
97
98    #[test]
99    fn already_exists_maps_to_structured_variant() {
100        let err = object_store::Error::AlreadyExists {
101            path: "worm/manifest.json".to_owned(),
102            source: "exists".into(),
103        };
104        let mapped = ObjectStoreError::from(err);
105        assert!(
106            matches!(&mapped, ObjectStoreError::AlreadyExists(p) if p.to_string() == "worm/manifest.json")
107        );
108    }
109
110    #[test]
111    fn precondition_maps_to_structured_variant() {
112        let err = object_store::Error::Precondition {
113            path: "worm/manifest.json".to_owned(),
114            source: "etag mismatch".into(),
115        };
116        let mapped = ObjectStoreError::from(err);
117        assert!(
118            matches!(&mapped, ObjectStoreError::Precondition { key, reason }
119                if key.to_string() == "worm/manifest.json" && reason == "etag mismatch")
120        );
121    }
122
123    #[test]
124    fn too_large_display_includes_sizes() {
125        let err = ObjectStoreError::TooLarge {
126            key: Path::from("index/snapshot.json"),
127            size: 1000,
128            max_bytes: 256,
129        };
130        let msg = err.to_string();
131        assert!(msg.contains("1000"));
132        assert!(msg.contains("256"));
133    }
134
135    #[test]
136    fn other_errors_map_to_backend() {
137        let err = object_store::Error::Generic {
138            store: "s",
139            source: "boom".into(),
140        };
141        assert!(matches!(
142            ObjectStoreError::from(err),
143            ObjectStoreError::Backend(_)
144        ));
145    }
146}