git_remote_object_store/object_store/error.rs
1//! Shared error type for every [`ObjectStore`][super::ObjectStore]
2//! implementation.
3//!
4//! Centralises the mapping of backend-specific failure codes onto a small,
5//! finite set of variants so higher layers (push, fetch, doctor, LFS) can
6//! pattern-match without caring whether the underlying SDK returned an
7//! `aws_sdk_s3::error::SdkError` or an `azure_core::error::Error`.
8//!
9//! On the conditional-write path, S3 returns 412 (`PreconditionFailed`)
10//! *and* 409 (`ConditionalRequestConflict`) for the same
11//! `If-None-Match: "*"` contention path; both variants are kept here so
12//! backends can preserve the distinction in diagnostics, while the
13//! `put_if_absent` trait method collapses both into the `Ok(false)`
14//! "lock not acquired" return.
15
16use std::error::Error as StdError;
17
18/// Boxed source error used by [`ObjectStoreError::Network`] and
19/// [`ObjectStoreError::Other`].
20///
21/// `Send + Sync + 'static` so the error can cross task boundaries; this
22/// matches the bounds `tokio::task::JoinHandle` and friends impose.
23pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
24
25/// Errors returned by every [`ObjectStore`][super::ObjectStore] method.
26///
27/// The `String` payload on the four key-correlated variants names the key
28/// (or, for `list`, the prefix) the operation was attempting, so
29/// `tracing::error!` lines remain actionable without the caller adding
30/// context.
31#[derive(Debug, thiserror::Error)]
32pub enum ObjectStoreError {
33 /// Object (or, for `list`, every object under the prefix) is absent.
34 #[error("object not found: {0}")]
35 NotFound(String),
36
37 /// Authentication succeeded but the principal is not allowed to perform
38 /// the operation. Maps from S3 `AccessDenied` (HTTP 403) and Azure
39 /// `AuthorizationFailure`.
40 #[error("access denied: {0}")]
41 AccessDenied(String),
42
43 /// Conditional request returned 412 — the precondition (typically
44 /// `If-None-Match: "*"`) was not satisfied. `put_if_absent`
45 /// collapses this into `Ok(false)`, so callers should rarely
46 /// observe it directly.
47 #[error("precondition failed: {0}")]
48 PreconditionFailed(String),
49
50 /// Conditional request returned 409. Treated by `put_if_absent` callers
51 /// the same as `PreconditionFailed`, but kept distinct for diagnostics.
52 #[error("conflict: {0}")]
53 Conflict(String),
54
55 /// Upload body exceeded the backend's size ceiling for the API call
56 /// in use. Maps from S3 `EntityTooLarge` (single-PUT > 5 GiB) and
57 /// Azure 413 / `RequestBodyTooLarge` (single Put Blob > 5000 MiB).
58 /// `limit_bytes` is the backend-specific ceiling at the time the
59 /// classifier ran, surfaced so the wire-line message names a concrete
60 /// number rather than dumping an opaque SDK error chain.
61 #[error("upload exceeds backend size limit ({})", format_byte_limit(*limit_bytes))]
62 PayloadTooLarge {
63 /// The backend's documented single-call size ceiling, in bytes.
64 limit_bytes: u64,
65 },
66
67 /// Ranged GET requested a byte range that the backend cannot
68 /// satisfy. Maps from HTTP 416 on both S3 and Azure, and surfaces
69 /// caller-side range bugs (`start > end`) without issuing a network
70 /// call. The `requested` range is the half-open `[start, end)` the
71 /// caller passed to `get_bytes_range`.
72 #[error("range {}..{} not satisfiable for key `{key}`", requested.start, requested.end)]
73 RangeNotSatisfiable {
74 /// Key being read.
75 key: String,
76 /// Range the caller asked for (half-open, end-exclusive).
77 requested: std::ops::Range<u64>,
78 },
79
80 /// Transport-level failure (DNS, TLS, timeout, connection reset).
81 /// Carries the original SDK error as `#[source]` so the chain is
82 /// preserved. The inner error is included in the display so the
83 /// SDK-level detail (e.g. "operation timed out") surfaces in the
84 /// push `error <ref>` wire line without requiring verbose logging.
85 #[error("network error: {0}")]
86 Network(#[source] BoxError),
87
88 /// Operation is not implemented for the backend in use. Used by
89 /// optional [`ObjectStore`](crate::object_store::ObjectStore)
90 /// methods such as `presigned_get_url` that not every backend
91 /// can satisfy (e.g. `MockStore` in tests, or `AzureStore`
92 /// configured with a `TokenCredential` rather than a shared
93 /// account key — the latter cannot generate a service-SAS). The
94 /// payload is the full operator-facing message; the variant
95 /// adds no prefix of its own so chained errors do not double-
96 /// say "not supported".
97 #[error("{0}")]
98 Unsupported(String),
99
100 /// Any backend failure that does not fit the variants above.
101 #[error(transparent)]
102 Other(BoxError),
103}
104
105/// Render a byte ceiling as the largest unit that divides it cleanly
106/// (`5 GiB`, `5000 MiB`, otherwise raw bytes). Used in
107/// [`ObjectStoreError::PayloadTooLarge`]'s `Display` so the wire-line
108/// reads as a familiar quota number rather than "5368709120".
109fn format_byte_limit(bytes: u64) -> String {
110 const GIB: u64 = 1 << 30;
111 const MIB: u64 = 1 << 20;
112 if bytes >= GIB && bytes.is_multiple_of(GIB) {
113 format!("{} GiB", bytes / GIB)
114 } else if bytes >= MIB && bytes.is_multiple_of(MIB) {
115 format!("{} MiB", bytes / MIB)
116 } else {
117 format!("{bytes} B")
118 }
119}
120
121/// Wrap any concrete `std::error::Error` into [`ObjectStoreError::Other`].
122///
123/// Replaces the open-coded `|e| ObjectStoreError::Other(Box::new(e))` closure
124/// that otherwise repeats at every I/O / time-conversion / persist
125/// call site.
126pub(crate) fn other_boxed<E: StdError + Send + Sync + 'static>(e: E) -> ObjectStoreError {
127 ObjectStoreError::Other(Box::new(e))
128}
129
130/// Wrap any concrete `std::error::Error` into [`ObjectStoreError::Network`].
131///
132/// Replaces the open-coded `|e| ObjectStoreError::Network(Box::new(e))`
133/// closure used at every body-streaming / multipart-chunk site that
134/// surfaces a transport failure.
135pub(crate) fn network_boxed<E: StdError + Send + Sync + 'static>(e: E) -> ObjectStoreError {
136 ObjectStoreError::Network(Box::new(e))
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn boxed_io(message: &str) -> BoxError {
144 Box::new(std::io::Error::other(message.to_string()))
145 }
146
147 #[test]
148 fn display_names_the_key() {
149 assert_eq!(
150 ObjectStoreError::NotFound("a/b".into()).to_string(),
151 "object not found: a/b"
152 );
153 assert_eq!(
154 ObjectStoreError::AccessDenied("a/b".into()).to_string(),
155 "access denied: a/b"
156 );
157 assert_eq!(
158 ObjectStoreError::PreconditionFailed("a/b".into()).to_string(),
159 "precondition failed: a/b"
160 );
161 assert_eq!(
162 ObjectStoreError::Conflict("a/b".into()).to_string(),
163 "conflict: a/b"
164 );
165 }
166
167 #[test]
168 fn network_preserves_source_chain() {
169 let err = ObjectStoreError::Network(boxed_io("dns failure"));
170 assert_eq!(err.to_string(), "network error: dns failure");
171 let source = err.source().expect("Network exposes its #[source]");
172 assert_eq!(source.to_string(), "dns failure");
173 }
174
175 #[test]
176 fn other_is_transparent() {
177 let err = ObjectStoreError::Other(boxed_io("boom"));
178 // `transparent` forwards Display to the inner error.
179 assert_eq!(err.to_string(), "boom");
180 }
181
182 #[test]
183 fn payload_too_large_renders_gib_when_exact() {
184 let err = ObjectStoreError::PayloadTooLarge {
185 limit_bytes: 5 * (1 << 30),
186 };
187 assert_eq!(err.to_string(), "upload exceeds backend size limit (5 GiB)");
188 }
189
190 #[test]
191 fn payload_too_large_renders_mib_when_not_a_clean_gib() {
192 // Azure single-PUT ceiling is 5000 MiB — close to but not 5 GiB.
193 let err = ObjectStoreError::PayloadTooLarge {
194 limit_bytes: 5_000 * (1 << 20),
195 };
196 assert_eq!(
197 err.to_string(),
198 "upload exceeds backend size limit (5000 MiB)"
199 );
200 }
201
202 #[test]
203 fn payload_too_large_falls_back_to_raw_bytes() {
204 let err = ObjectStoreError::PayloadTooLarge { limit_bytes: 1_234 };
205 assert_eq!(
206 err.to_string(),
207 "upload exceeds backend size limit (1234 B)"
208 );
209 }
210
211 #[test]
212 fn range_not_satisfiable_names_key_and_range() {
213 let err = ObjectStoreError::RangeNotSatisfiable {
214 key: "packs/abc.pack".to_string(),
215 requested: 100..200,
216 };
217 assert_eq!(
218 err.to_string(),
219 "range 100..200 not satisfiable for key `packs/abc.pack`"
220 );
221 }
222}