1pub type Result<T, E = Error> = std::result::Result<T, E>;
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum Error {
17 #[error("not found: {uri}")]
19 NotFound {
20 uri: String,
22 },
23
24 #[error("not modified: {uri}")]
27 NotModified {
28 uri: String,
30 },
31
32 #[error("authentication failed: {message}")]
34 Unauthenticated {
35 message: String,
37 },
38
39 #[error("permission denied: {message}")]
41 PermissionDenied {
42 message: String,
44 },
45
46 #[error("rate limited; retry after {retry_after_ms}ms")]
48 RateLimited {
49 retry_after_ms: u64,
51 },
52
53 #[error("HTTP {status}: {message}")]
55 Http {
56 status: u16,
58 message: String,
60 },
61
62 #[error("invalid URL: {message}")]
64 InvalidUrl {
65 message: String,
67 },
68
69 #[error("crawl failed [{source_kind}]: {message}")]
71 Crawl {
72 source_kind: &'static str,
74 message: String,
76 },
77
78 #[error("extraction failed: {message}")]
80 Extract {
81 message: String,
83 },
84
85 #[error("embedding failed: {message}")]
87 Embed {
88 message: String,
90 },
91
92 #[error("index format error: {message}")]
94 Format {
95 message: String,
97 },
98
99 #[error("dimension mismatch: index={index}, query={query}")]
101 DimMismatch {
102 index: usize,
104 query: usize,
106 },
107
108 #[error("operation timed out after {duration_ms}ms")]
110 Timeout {
111 duration_ms: u64,
113 },
114
115 #[error("{backend} error: {message}")]
117 Backend {
118 backend: &'static str,
120 message: String,
122 },
123
124 #[error("I/O error: {source}")]
126 Io {
127 #[from]
129 source: std::io::Error,
130 },
131}
132
133impl From<graph_r::Error> for Error {
136 fn from(e: graph_r::Error) -> Self {
137 match e {
138 graph_r::Error::Io { source } => Self::Io { source },
139 other => Self::format(other.to_string()),
140 }
141 }
142}
143
144impl Error {
145 #[must_use]
147 pub fn not_found(uri: impl Into<String>) -> Self {
148 Self::NotFound { uri: uri.into() }
149 }
150
151 #[must_use]
153 pub fn not_modified(uri: impl Into<String>) -> Self {
154 Self::NotModified { uri: uri.into() }
155 }
156
157 #[must_use]
159 pub fn unauthenticated(message: impl Into<String>) -> Self {
160 Self::Unauthenticated {
161 message: message.into(),
162 }
163 }
164
165 #[must_use]
167 pub fn permission_denied(message: impl Into<String>) -> Self {
168 Self::PermissionDenied {
169 message: message.into(),
170 }
171 }
172
173 #[must_use]
175 pub fn rate_limited(retry_after_ms: u64) -> Self {
176 Self::RateLimited { retry_after_ms }
177 }
178
179 #[must_use]
181 pub fn http(status: u16, message: impl Into<String>) -> Self {
182 Self::Http {
183 status,
184 message: message.into(),
185 }
186 }
187
188 #[must_use]
190 pub fn invalid_url(message: impl Into<String>) -> Self {
191 Self::InvalidUrl {
192 message: message.into(),
193 }
194 }
195
196 #[must_use]
198 pub fn crawl(source_kind: &'static str, message: impl Into<String>) -> Self {
199 Self::Crawl {
200 source_kind,
201 message: message.into(),
202 }
203 }
204
205 #[must_use]
207 pub fn extract(message: impl Into<String>) -> Self {
208 Self::Extract {
209 message: message.into(),
210 }
211 }
212
213 #[must_use]
215 pub fn embed(message: impl Into<String>) -> Self {
216 Self::Embed {
217 message: message.into(),
218 }
219 }
220
221 #[must_use]
223 pub fn format(message: impl Into<String>) -> Self {
224 Self::Format {
225 message: message.into(),
226 }
227 }
228
229 #[must_use]
231 pub fn backend(backend: &'static str, message: impl Into<String>) -> Self {
232 Self::Backend {
233 backend,
234 message: message.into(),
235 }
236 }
237
238 #[must_use]
244 pub fn is_retriable(&self) -> bool {
245 match self {
246 Self::Timeout { .. } | Self::RateLimited { .. } => true,
247 Self::Http { status, .. } => objstore_r::net::is_retriable_status(*status),
248 Self::Io { source } => matches!(
249 source.kind(),
250 std::io::ErrorKind::ConnectionReset
251 | std::io::ErrorKind::ConnectionAborted
252 | std::io::ErrorKind::TimedOut
253 | std::io::ErrorKind::Interrupted
254 | std::io::ErrorKind::WouldBlock
255 ),
256 _ => false,
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn constructors_build_expected_variants() {
267 assert!(matches!(Error::not_found("x"), Error::NotFound { .. }));
268 assert!(matches!(
269 Error::http(503, "x"),
270 Error::Http { status: 503, .. }
271 ));
272 assert!(matches!(
273 Error::backend("reqwest", "boom"),
274 Error::Backend {
275 backend: "reqwest",
276 ..
277 }
278 ));
279 }
280
281 #[test]
282 fn retriable_classification() {
283 assert!(Error::rate_limited(100).is_retriable());
284 assert!(Error::Timeout { duration_ms: 1 }.is_retriable());
285 assert!(Error::http(503, "").is_retriable());
286 assert!(Error::http(500, "").is_retriable());
287 assert!(!Error::http(404, "").is_retriable());
288 assert!(!Error::not_found("x").is_retriable());
289 assert!(!Error::unauthenticated("x").is_retriable());
290 }
291
292 #[test]
293 fn io_errors_convert_via_from() {
294 let io = std::io::Error::new(std::io::ErrorKind::TimedOut, "slow");
295 let err: Error = io.into();
296 assert!(err.is_retriable());
297 assert!(matches!(err, Error::Io { .. }));
298 }
299
300 #[test]
301 fn display_is_human_readable() {
302 assert_eq!(
303 Error::not_found("https://x/y").to_string(),
304 "not found: https://x/y"
305 );
306 }
307}