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}