Skip to main content

xet/
error.rs

1use thiserror::Error;
2use xet_client::ClientError;
3use xet_core_structures::CoreError;
4use xet_data::DataError;
5use xet_data::file_reconstruction::FileReconstructionError;
6use xet_data::progress_tracking::UniqueID;
7use xet_runtime::RuntimeError;
8
9/// Unified error type for the Xet public API.
10///
11/// Variants are grouped into user-facing categories that map naturally to
12/// Python exception types, plus session-lifecycle states that the internal
13/// code can match on.
14#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum XetError {
17    // -- Session lifecycle -----------------------------------------------
18    /// SIGINT / runtime shutdown.
19    #[error("Keyboard interrupt (SIGINT)")]
20    KeyboardInterrupt,
21
22    /// Explicit user abort (session, commit, group, or stream level).
23    #[error("User cancelled: {0}")]
24    UserCancelled(String),
25
26    /// A previous operation on this task failed; carries the stored error description.
27    #[error("Previous task error: {0}")]
28    PreviousTaskError(String),
29
30    /// Task-level error captured from a background upload/download handle.
31    #[error("Task error: {0}")]
32    TaskError(String),
33
34    /// The operation has already completed, is already finalizing, or was already committed/finished.
35    #[error("Already completed")]
36    AlreadyCompleted,
37
38    /// A task ID that doesn't correspond to any queued file.
39    #[error("Invalid task ID: {0}")]
40    InvalidTaskID(UniqueID),
41
42    // -- User-facing error categories ------------------------------------
43    /// Token refresh or credential failures.
44    #[error("Authentication error: {0}")]
45    Authentication(String),
46
47    /// Network-level failures: DNS, HTTP 5xx, connection reset, etc.
48    #[error("Network error: {0}")]
49    Network(String),
50
51    /// A network request timed out.
52    #[error("Timeout: {0}")]
53    Timeout(String),
54
55    /// A requested resource (file, XORB, shard) does not exist.
56    #[error("Not found: {0}")]
57    NotFound(String),
58
59    /// Data corruption: hash mismatches, invalid shard/xorb format, etc.
60    #[error("Data integrity error: {0}")]
61    DataIntegrity(String),
62
63    /// Invalid configuration or arguments supplied by the caller.
64    #[error("Configuration error: {0}")]
65    Configuration(String),
66
67    /// Local filesystem I/O failures.
68    #[error("I/O error: {0}")]
69    Io(String),
70
71    /// Generic cancellation from non-user sources (semaphore close, join cancellation).
72    #[error("Operation cancelled: {0}")]
73    Cancelled(String),
74
75    /// Catch-all for unexpected internal errors (panics, lock poison, bugs).
76    #[error("Internal error: {0}")]
77    Internal(String),
78
79    /// Caller invoked a method that is incompatible with the session's runtime mode.
80    #[error("Wrong runtime mode: {0}")]
81    WrongRuntimeMode(String),
82}
83
84impl XetError {
85    pub fn other(msg: impl std::fmt::Display) -> Self {
86        Self::Internal(msg.to_string())
87    }
88
89    pub fn wrong_mode(msg: impl std::fmt::Display) -> Self {
90        Self::WrongRuntimeMode(msg.to_string())
91    }
92
93    fn from_runtime_error_ref(re: &RuntimeError) -> Self {
94        match re {
95            RuntimeError::KeyboardInterrupt => XetError::KeyboardInterrupt,
96            RuntimeError::TaskCanceled(msg) => XetError::Cancelled(format!("Task cancelled: {msg}")),
97            RuntimeError::InvalidRuntime(_) => XetError::WrongRuntimeMode(re.to_string()),
98            _ => XetError::Internal(re.to_string()),
99        }
100    }
101
102    fn from_core_error_ref(fe: &CoreError) -> Self {
103        match fe {
104            CoreError::Io(_) => XetError::Io(fe.to_string()),
105            CoreError::ShardNotFound(_) | CoreError::FileNotFound(_) => XetError::NotFound(fe.to_string()),
106            CoreError::HashMismatch
107            | CoreError::TruncatedHashCollision(_)
108            | CoreError::InvalidShard(_)
109            | CoreError::ShardVersion(_)
110            | CoreError::ChunkHeaderParse
111            | CoreError::MalformedData(_)
112            | CoreError::CompressionError(_) => XetError::DataIntegrity(fe.to_string()),
113            CoreError::InvalidRange | CoreError::InvalidArguments | CoreError::BadFilename(_) => {
114                XetError::Configuration(fe.to_string())
115            },
116            CoreError::RuntimeError(re) => XetError::from_runtime_error_ref(re),
117            _ => XetError::Internal(fe.to_string()),
118        }
119    }
120
121    fn from_client_error_ref(ce: &ClientError) -> Self {
122        match ce {
123            ClientError::AuthError(_) | ClientError::PresignedUrlExpirationError | ClientError::CredentialHelper(_) => {
124                XetError::Authentication(ce.to_string())
125            },
126            ClientError::ReqwestError(e, _) if e.is_timeout() => XetError::Timeout(ce.to_string()),
127            ClientError::ReqwestError(_, _) | ClientError::ReqwestMiddlewareError(_) => {
128                XetError::Network(ce.to_string())
129            },
130            ClientError::FileNotFound(_) | ClientError::XORBNotFound(_) => XetError::NotFound(ce.to_string()),
131            ClientError::ConfigurationError(_)
132            | ClientError::InvalidArguments
133            | ClientError::InvalidRange
134            | ClientError::InvalidShardKey(_)
135            | ClientError::InvalidKey(_)
136            | ClientError::InvalidRepoType(_) => XetError::Configuration(ce.to_string()),
137            ClientError::IOError(_) => XetError::Io(ce.to_string()),
138            ClientError::FormatError(fe) => XetError::from_core_error_ref(fe),
139            _ => XetError::Internal(ce.to_string()),
140        }
141    }
142
143    fn from_file_reconstruction_error_ref(fre: &FileReconstructionError) -> Self {
144        match fre {
145            FileReconstructionError::ClientError(ce) => XetError::from_client_error_ref(ce),
146            FileReconstructionError::IoError(_) => XetError::Io(fre.to_string()),
147            FileReconstructionError::RuntimeError(re) => XetError::from_runtime_error_ref(re),
148            FileReconstructionError::TaskJoinError(je) if je.is_cancelled() => {
149                XetError::Cancelled(format!("Task cancelled: {je}"))
150            },
151            FileReconstructionError::TaskJoinError(je) => XetError::Internal(format!("Task join error: {je}")),
152            FileReconstructionError::ConfigurationError(_) => XetError::Configuration(fre.to_string()),
153            FileReconstructionError::CorruptedReconstruction(_) => XetError::DataIntegrity(fre.to_string()),
154            _ => XetError::Internal(fre.to_string()),
155        }
156    }
157
158    fn from_data_error_ref(de: &DataError) -> Self {
159        match de {
160            DataError::AuthError(_) => XetError::Authentication(de.to_string()),
161            DataError::ClientError(ce) => XetError::from_client_error_ref(ce),
162            DataError::FormatError(fe) => XetError::from_core_error_ref(fe),
163            DataError::IOError(_) => XetError::Io(de.to_string()),
164            DataError::RuntimeError(re) => XetError::from_runtime_error_ref(re),
165            DataError::FileQueryPolicyError(_)
166            | DataError::CASConfigError(_)
167            | DataError::ShardConfigError(_)
168            | DataError::DedupConfigError(_)
169            | DataError::ParameterError(_)
170            | DataError::DeprecatedError(_) => XetError::Configuration(de.to_string()),
171            DataError::HashNotFound => XetError::NotFound(de.to_string()),
172            DataError::HashStringParsingFailure(_) => XetError::DataIntegrity(de.to_string()),
173            DataError::InvalidOperation(_) => XetError::Configuration(de.to_string()),
174            DataError::FileReconstructionError(fre) => XetError::from_file_reconstruction_error_ref(fre),
175            _ => XetError::Internal(de.to_string()),
176        }
177    }
178}
179
180// -- From impls for package-level errors ---------------------------------
181
182impl From<RuntimeError> for XetError {
183    fn from(e: RuntimeError) -> Self {
184        XetError::from_runtime_error_ref(&e)
185    }
186}
187
188impl From<CoreError> for XetError {
189    fn from(e: CoreError) -> Self {
190        XetError::from_core_error_ref(&e)
191    }
192}
193
194impl From<ClientError> for XetError {
195    fn from(e: ClientError) -> Self {
196        XetError::from_client_error_ref(&e)
197    }
198}
199
200impl From<DataError> for XetError {
201    fn from(e: DataError) -> Self {
202        XetError::from_data_error_ref(&e)
203    }
204}
205
206impl From<FileReconstructionError> for XetError {
207    fn from(e: FileReconstructionError) -> Self {
208        XetError::from_file_reconstruction_error_ref(&e)
209    }
210}
211
212// -- Convenience From impls for common error types -----------------------
213
214impl From<std::io::Error> for XetError {
215    fn from(e: std::io::Error) -> Self {
216        XetError::Io(e.to_string())
217    }
218}
219
220impl From<tokio::task::JoinError> for XetError {
221    fn from(e: tokio::task::JoinError) -> Self {
222        if e.is_cancelled() {
223            XetError::Cancelled(format!("Task cancelled: {e}"))
224        } else {
225            XetError::Internal(format!("Task join error: {e}"))
226        }
227    }
228}
229
230impl From<tokio::sync::AcquireError> for XetError {
231    fn from(e: tokio::sync::AcquireError) -> Self {
232        XetError::Cancelled(format!("Semaphore closed: {e}"))
233    }
234}
235
236impl<T> From<std::sync::PoisonError<std::sync::MutexGuard<'_, T>>> for XetError {
237    fn from(e: std::sync::PoisonError<std::sync::MutexGuard<'_, T>>) -> Self {
238        XetError::Internal(format!("Mutex poisoned: {e}"))
239    }
240}
241
242impl<T> From<std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, T>>> for XetError {
243    fn from(e: std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, T>>) -> Self {
244        XetError::Internal(format!("RwLock write poisoned: {e}"))
245    }
246}
247
248impl<T> From<std::sync::PoisonError<std::sync::RwLockReadGuard<'_, T>>> for XetError {
249    fn from(e: std::sync::PoisonError<std::sync::RwLockReadGuard<'_, T>>) -> Self {
250        XetError::Internal(format!("RwLock read poisoned: {e}"))
251    }
252}
253
254// -- Python exception classes & conversion --------------------------------
255
256#[cfg(feature = "python")]
257mod py_exceptions {
258    // Inherits from Python's PermissionError so `except PermissionError` still catches it.
259    pyo3::create_exception!(hf_xet, XetAuthenticationError, pyo3::exceptions::PyPermissionError);
260
261    // Inherits from Python's FileNotFoundError so `except FileNotFoundError` still catches it.
262    pyo3::create_exception!(hf_xet, XetObjectNotFoundError, pyo3::exceptions::PyFileNotFoundError);
263
264    /// Register the custom exception classes on a Python module.
265    ///
266    /// Call this from the `#[pymodule]` init function so that the exceptions
267    /// are importable as `hf_xet.XetAuthenticationError`, etc.
268    pub fn register_exceptions(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
269        use pyo3::types::PyModuleMethods;
270
271        m.add("XetAuthenticationError", m.py().get_type::<XetAuthenticationError>())?;
272        m.add("XetObjectNotFoundError", m.py().get_type::<XetObjectNotFoundError>())?;
273        Ok(())
274    }
275}
276
277#[cfg(feature = "python")]
278pub use py_exceptions::{XetAuthenticationError, XetObjectNotFoundError, register_exceptions};
279
280#[cfg(feature = "python")]
281impl From<XetError> for pyo3::PyErr {
282    fn from(err: XetError) -> pyo3::PyErr {
283        use pyo3::exceptions::{
284            PyConnectionError, PyKeyboardInterrupt, PyOSError, PyRuntimeError, PyTimeoutError, PyValueError,
285        };
286
287        let msg = err.to_string();
288        #[allow(unreachable_patterns)] // XetError is #[non_exhaustive]
289        match err {
290            XetError::KeyboardInterrupt => PyKeyboardInterrupt::new_err(msg),
291            XetError::Authentication(_) => XetAuthenticationError::new_err(msg),
292            XetError::NotFound(_) => XetObjectNotFoundError::new_err(msg),
293            XetError::Network(_) => PyConnectionError::new_err(msg),
294            XetError::Timeout(_) => PyTimeoutError::new_err(msg),
295            XetError::Io(_) => PyOSError::new_err(msg),
296            XetError::Configuration(_) | XetError::InvalidTaskID(_) => PyValueError::new_err(msg),
297            XetError::DataIntegrity(_)
298            | XetError::Internal(_)
299            | XetError::WrongRuntimeMode(_)
300            | XetError::AlreadyCompleted
301            | XetError::UserCancelled(_)
302            | XetError::PreviousTaskError(_)
303            | XetError::TaskError(_)
304            | XetError::Cancelled(_) => PyRuntimeError::new_err(msg),
305            _ => PyRuntimeError::new_err(msg),
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use xet_client::cas_client::auth::AuthError;
313    use xet_core_structures::merklehash::MerkleHash;
314
315    use super::*;
316
317    #[test]
318    fn runtime_cancelled_maps_to_cancelled() {
319        let err = XetError::from(RuntimeError::TaskCanceled("worker stopped".to_string()));
320        assert!(matches!(err, XetError::Cancelled(_)));
321    }
322
323    #[test]
324    fn runtime_keyboard_interrupt_maps_to_keyboard_interrupt() {
325        let err = XetError::from(RuntimeError::KeyboardInterrupt);
326        assert!(matches!(err, XetError::KeyboardInterrupt));
327    }
328
329    #[test]
330    fn format_not_found_maps_to_not_found() {
331        let err = XetError::from(CoreError::ShardNotFound(MerkleHash::default()));
332        assert!(matches!(err, XetError::NotFound(_)));
333    }
334
335    #[test]
336    fn format_invalid_args_maps_to_configuration() {
337        let err = XetError::from(CoreError::InvalidArguments);
338        assert!(matches!(err, XetError::Configuration(_)));
339    }
340
341    #[test]
342    fn client_auth_maps_to_authentication() {
343        let err = XetError::from(ClientError::AuthError(AuthError::TokenRefreshFailure("bad token".to_string())));
344        assert!(matches!(err, XetError::Authentication(_)));
345    }
346
347    #[test]
348    fn client_nested_format_maps_using_format_rules() {
349        let err = XetError::from(ClientError::FormatError(CoreError::InvalidRange));
350        assert!(matches!(err, XetError::Configuration(_)));
351    }
352
353    #[test]
354    fn data_nested_client_maps_using_client_rules() {
355        let err = XetError::from(DataError::ClientError(ClientError::FileNotFound(MerkleHash::default())));
356        assert!(matches!(err, XetError::NotFound(_)));
357    }
358
359    #[test]
360    fn data_runtime_cancelled_maps_to_cancelled() {
361        let err = XetError::from(DataError::RuntimeError(RuntimeError::TaskCanceled("cancelled".to_string())));
362        assert!(matches!(err, XetError::Cancelled(_)));
363    }
364
365    #[test]
366    fn presigned_url_expiration_maps_to_authentication() {
367        let err = XetError::from(ClientError::PresignedUrlExpirationError);
368        assert!(matches!(err, XetError::Authentication(_)));
369    }
370
371    #[test]
372    fn credential_helper_maps_to_authentication() {
373        let err = XetError::from(ClientError::credential_helper_error(std::io::Error::other("cred fail")));
374        assert!(matches!(err, XetError::Authentication(_)));
375    }
376
377    #[test]
378    fn client_not_found_maps_to_not_found() {
379        let err = XetError::from(ClientError::FileNotFound(MerkleHash::default()));
380        assert!(matches!(err, XetError::NotFound(_)));
381    }
382
383    #[test]
384    fn client_xorb_not_found_maps_to_not_found() {
385        let err = XetError::from(ClientError::XORBNotFound(MerkleHash::default()));
386        assert!(matches!(err, XetError::NotFound(_)));
387    }
388
389    #[test]
390    fn client_io_maps_to_io() {
391        let err = XetError::from(ClientError::IOError(std::io::Error::new(std::io::ErrorKind::NotFound, "gone")));
392        assert!(matches!(err, XetError::Io(_)));
393    }
394}