Skip to main content

git_lfs_transfer/
error.rs

1use git_lfs_api::{ApiError, ObjectError};
2use git_lfs_pointer::OidParseError;
3use git_lfs_store::StoreError;
4
5/// Why a per-object transfer failed.
6///
7/// Errors with `is_retryable() == true` are retried by the queue up to
8/// [`TransferConfig::max_attempts`](crate::TransferConfig::max_attempts);
9/// everything else fails fast.
10#[derive(Debug, thiserror::Error)]
11pub enum TransferError {
12    /// The batch endpoint returned a per-object error (404, 410, 422, …).
13    /// Not retryable: the server has already classified the object.
14    #[error("server error for object: {} ({})", .0.message, .0.code)]
15    ServerObject(ObjectError),
16
17    /// The batch response listed the object with neither `actions` nor
18    /// `error` for a download — the spec forbids this, but real servers do
19    /// it occasionally; we surface it instead of panicking.
20    #[error("server returned no download action for object")]
21    NoDownloadAction,
22
23    /// The action URL responded with a non-success status.
24    #[error("transfer action returned status {status}")]
25    ActionStatus { status: u16 },
26
27    /// HTTP transport failure (connection reset, TLS error, …).
28    /// Retryable.
29    #[error("http error: {0}")]
30    Http(#[from] reqwest::Error),
31
32    /// Local I/O while reading the object file (uploads) or the staging
33    /// tempfile (downloads).
34    #[error("local io error: {0}")]
35    Io(#[from] std::io::Error),
36
37    /// The local store rejected the bytes — most importantly, hash mismatch
38    /// after a download. Not retryable per attempt: the bytes the server
39    /// gave us did not hash to what they promised.
40    #[error("store error: {0}")]
41    Store(#[from] StoreError),
42
43    /// The OID returned by the server is not valid hex.
44    #[error("invalid oid from server: {0}")]
45    InvalidOid(#[from] OidParseError),
46}
47
48impl TransferError {
49    /// Worth another attempt? Network blips and 5xx are retryable; spec
50    /// violations and hash mismatches are not.
51    pub fn is_retryable(&self) -> bool {
52        match self {
53            TransferError::Http(e) => {
54                // reqwest::Error doesn't expose enough to be precise — treat
55                // any non-decode transport error as retryable. Hash mismatch
56                // surfaces via Store, not Http.
57                !e.is_decode() && !e.is_builder()
58            }
59            TransferError::ActionStatus { status } => {
60                matches!(status, 408 | 429 | 500..=599)
61            }
62            TransferError::Io(_) => true,
63            TransferError::ServerObject(_)
64            | TransferError::NoDownloadAction
65            | TransferError::Store(_)
66            | TransferError::InvalidOid(_) => false,
67        }
68    }
69}
70
71impl From<ApiError> for TransferError {
72    fn from(value: ApiError) -> Self {
73        match value {
74            ApiError::Transport(e) => TransferError::Http(e),
75            other => {
76                // Typed Status / Decode / Url. Wrap as Io with the original
77                // message — this only fires on the batch call, which is
78                // upstream of per-object retry, so we never inspect this.
79                TransferError::Io(std::io::Error::other(other.to_string()))
80            }
81        }
82    }
83}
84
85/// Aggregate outcome of a transfer batch.
86#[derive(Debug, Default)]
87pub struct Report {
88    /// OIDs of objects that completed successfully.
89    pub succeeded: Vec<String>,
90    /// OIDs and reasons for objects that ultimately failed.
91    pub failed: Vec<(String, TransferError)>,
92}
93
94impl Report {
95    pub fn is_complete_success(&self) -> bool {
96        self.failed.is_empty()
97    }
98}