objectstore_service/error.rs
1//! Error types for service and backend operations.
2//!
3//! [`Error`] covers I/O, serialization, HTTP, metadata, authentication,
4//! and backend-specific failures. [`Result`] is the corresponding alias.
5
6use std::any::Any;
7use std::fmt;
8
9use objectstore_log::Level;
10use reqwest::StatusCode;
11use thiserror::Error as ThisError;
12
13use crate::stream::ClientError;
14
15/// Structured error detail parsed from a backend HTTP error response.
16///
17/// Formats conditionally: includes only the fields that are non-empty.
18#[derive(Debug)]
19pub struct BackendDetail {
20 /// Machine-readable error code (e.g., "InvalidArgument", "NoSuchKey").
21 pub code: String,
22 /// Human-readable error message from the response body.
23 pub message: String,
24}
25
26impl BackendDetail {
27 /// Creates a new [`BackendDetail`] with empty code and message.
28 pub fn none() -> Self {
29 Self {
30 code: String::new(),
31 message: String::new(),
32 }
33 }
34}
35
36impl fmt::Display for BackendDetail {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match (self.code.is_empty(), self.message.is_empty()) {
39 (false, false) => write!(f, "{} (backend code {})", self.message, self.code),
40 (true, false) => write!(f, "{}", self.message),
41 (false, true) => write!(f, "backend code {}", self.code),
42 (true, true) => Ok(()),
43 }
44 }
45}
46
47/// Error type for service operations.
48#[derive(Debug, ThisError)]
49pub enum Error {
50 /// IO errors related to payload streaming or file operations.
51 #[error("i/o error: {0}")]
52 Io(#[from] std::io::Error),
53
54 /// Error originating from a client-supplied input stream.
55 ///
56 /// Indicates the client is at fault (e.g. dropped connection mid-upload) and should
57 /// map to a 4xx response rather than a 5xx.
58 #[error("error reading client stream: {0}")]
59 Client(#[from] ClientError),
60
61 /// Errors related to de/serialization.
62 #[error("serde error: {context}")]
63 Serde {
64 /// Context describing what was being serialized/deserialized.
65 context: String,
66 /// The underlying serde error.
67 #[source]
68 cause: serde_json::Error,
69 },
70
71 /// All errors stemming from the reqwest client, used in multiple backends to send requests to
72 /// e.g. GCP APIs.
73 /// These can be network errors encountered when sending the requests, but can also indicate
74 /// errors returned by the API itself.
75 #[error("reqwest error: {context}")]
76 Reqwest {
77 /// Context describing the request that failed.
78 context: String,
79 /// The underlying reqwest error.
80 #[source]
81 cause: reqwest::Error,
82 },
83
84 /// An HTTP error response from a storage backend (e.g., GCS, S3).
85 ///
86 /// Unlike [`Reqwest`](Self::Reqwest), which covers transport-level failures, this variant
87 /// captures application-level error responses where the server returned a 4xx/5xx status code
88 /// along with a structured error body.
89 #[error("{context} ({status}). {detail}")]
90 BackendResponse {
91 /// Context describing the request that failed.
92 context: &'static str,
93 /// The HTTP status code returned by the backend.
94 status: StatusCode,
95 /// Parsed error code and message from the response body.
96 detail: BackendDetail,
97 },
98
99 /// Errors related to de/serialization and parsing of object metadata.
100 #[error("metadata error: {0}")]
101 Metadata(#[from] objectstore_types::metadata::Error),
102
103 /// Errors encountered when attempting to authenticate with GCP.
104 #[error("GCP authentication error: {0}")]
105 GcpAuth(#[from] gcp_auth::Error),
106
107 /// A spawned service task panicked.
108 #[error("service task failed: {0}")]
109 Panic(String),
110
111 /// A spawned service task was dropped before it could deliver its result.
112 ///
113 /// This is an unexpected condition that can occur when the runtime drops the task for unknown
114 /// reasons.
115 #[error("task dropped")]
116 Dropped,
117
118 /// A redirect tombstone was encountered at a place where it is not supported.
119 ///
120 /// This indicates a caller bug — tombstone-aware reads must go through the
121 /// [`HighVolumeBackend`](crate::backend::common::HighVolumeBackend) methods.
122 #[error("unexpected tombstone")]
123 UnexpectedTombstone,
124
125 /// The requested byte range is not satisfiable for the object's size.
126 #[error("range not satisfiable (object size: {total} bytes)")]
127 RangeNotSatisfiable {
128 /// Total size of the object in bytes.
129 total: u64,
130 },
131
132 /// The service has reached its concurrency limit and cannot accept more operations.
133 #[error("concurrency limit reached")]
134 AtCapacity,
135
136 /// Any other error stemming from one of the storage backends, which might be specific to that
137 /// backend or to a certain operation.
138 #[error("storage backend error: {context}")]
139 Generic {
140 /// Context describing the operation that failed.
141 context: String,
142 /// The underlying error, if available.
143 #[source]
144 cause: Option<Box<dyn std::error::Error + Send + Sync>>,
145 },
146
147 /// The functionality is not implemented by this instance of the service.
148 #[error("not implemented")]
149 NotImplemented,
150
151 /// Invalid upload ID (e.g. path traversal attempt).
152 #[error(transparent)]
153 InvalidUploadId(#[from] objectstore_types::multipart::InvalidUploadId),
154}
155
156impl Error {
157 /// Creates an [`Error::Panic`] from a panic payload, extracting the message.
158 pub fn panic(payload: Box<dyn Any + Send>) -> Self {
159 let msg = if let Some(s) = payload.downcast_ref::<&str>() {
160 (*s).to_owned()
161 } else if let Some(s) = payload.downcast_ref::<String>() {
162 s.clone()
163 } else {
164 "unknown panic".to_owned()
165 };
166 Self::Panic(msg)
167 }
168
169 /// Creates an [`Error::Reqwest`] from a reqwest error with context.
170 pub fn reqwest(context: impl Into<String>, cause: reqwest::Error) -> Self {
171 Self::Reqwest {
172 context: context.into(),
173 cause,
174 }
175 }
176
177 /// Creates an [`Error::Serde`] from a serde error with context.
178 pub fn serde(context: impl Into<String>, cause: serde_json::Error) -> Self {
179 Self::Serde {
180 context: context.into(),
181 cause,
182 }
183 }
184
185 /// Creates an [`Error::Generic`] with a context string and no cause.
186 pub fn generic(context: impl Into<String>) -> Self {
187 Self::Generic {
188 context: context.into(),
189 cause: None,
190 }
191 }
192
193 /// Returns the appropriate log level for this error.
194 pub fn level(&self) -> Level {
195 match self {
196 // Malformed client input at DEBUG level
197 Self::Client(_) => Level::DEBUG,
198 Self::Metadata(_) => Level::DEBUG,
199 Self::RangeNotSatisfiable { .. } => Level::DEBUG,
200 // Like rate limits, we treat capacity errors as warnings
201 Self::AtCapacity => Level::WARN,
202 // All other errors are service or backend failures
203 Self::Io(_) => Level::ERROR,
204 Self::Serde { .. } => Level::ERROR,
205 Self::Reqwest { .. } => Level::ERROR,
206 Self::BackendResponse { .. } => Level::ERROR,
207 Self::GcpAuth(_) => Level::ERROR,
208 Self::Panic(_) => Level::ERROR,
209 Self::Dropped => Level::ERROR,
210 Self::UnexpectedTombstone => Level::ERROR,
211 Self::NotImplemented => Level::ERROR,
212 Self::InvalidUploadId(_) => Level::DEBUG,
213 Self::Generic { .. } => Level::ERROR,
214 }
215 }
216}
217
218/// Result type for service operations.
219pub type Result<T, E = Error> = std::result::Result<T, E>;