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. The URL is
24 /// embedded in the [`Display`](std::fmt::Display) impl so users can
25 /// see *which* endpoint failed (in particular, what `insteadOf`
26 /// rewriting did to the original batch URL — see t-pull's
27 /// `pull with invalid insteadof`).
28 #[error("{}", format_action_status(*.status, .url))]
29 ActionStatus { status: u16, url: String },
30
31 /// HTTP transport failure (connection reset, TLS error, …).
32 /// Retryable.
33 #[error("http error: {0}")]
34 Http(#[from] reqwest::Error),
35
36 /// Local I/O while reading the object file (uploads) or the staging
37 /// tempfile (downloads).
38 #[error("local io error: {0}")]
39 Io(#[from] std::io::Error),
40
41 /// The local store rejected the bytes — most importantly, hash mismatch
42 /// after a download. Not retryable per attempt: the bytes the server
43 /// gave us did not hash to what they promised.
44 #[error("store error: {0}")]
45 Store(#[from] StoreError),
46
47 /// The OID returned by the server is not valid hex.
48 #[error("invalid oid from server: {0}")]
49 InvalidOid(#[from] OidParseError),
50}
51
52/// Format the action-URL error message to match upstream's
53/// `lfshttp.defaultError` strings — the test suite greps these
54/// verbatim (e.g. t-pull's `pull with invalid insteadof`).
55fn format_action_status(status: u16, url: &str) -> String {
56 let prefix = match status {
57 400 => "Client error:",
58 401 | 403 => "Authorization error:",
59 404 => "Repository or object not found:",
60 422 => "Unprocessable entity:",
61 429 => "Rate limit exceeded:",
62 500 => "Server error:",
63 501 => "Not Implemented:",
64 507 => "Insufficient server storage:",
65 509 => "Bandwidth limit exceeded:",
66 _ if status < 500 => return format!("LFS: Client error {url} from HTTP {status}"),
67 _ => return format!("LFS: Server error {url} from HTTP {status}"),
68 };
69 format!("LFS: {prefix} {url}")
70}
71
72impl TransferError {
73 /// Worth another attempt? Network blips and 5xx are retryable; spec
74 /// violations and hash mismatches are not.
75 pub fn is_retryable(&self) -> bool {
76 match self {
77 TransferError::Http(e) => {
78 // reqwest::Error doesn't expose enough to be precise — treat
79 // any non-decode transport error as retryable. Hash mismatch
80 // surfaces via Store, not Http.
81 !e.is_decode() && !e.is_builder()
82 }
83 TransferError::ActionStatus { status, .. } => {
84 matches!(status, 408 | 429 | 500..=599)
85 }
86 TransferError::Io(_) => true,
87 TransferError::ServerObject(_)
88 | TransferError::NoDownloadAction
89 | TransferError::Store(_)
90 | TransferError::InvalidOid(_) => false,
91 }
92 }
93}
94
95impl From<ApiError> for TransferError {
96 fn from(value: ApiError) -> Self {
97 match value {
98 ApiError::Transport(e) => TransferError::Http(e),
99 other => {
100 // Typed Status / Decode / Url. Wrap as Io with the original
101 // message — this only fires on the batch call, which is
102 // upstream of per-object retry, so we never inspect this.
103 TransferError::Io(std::io::Error::other(other.to_string()))
104 }
105 }
106 }
107}
108
109/// Aggregate outcome of a transfer batch.
110#[derive(Debug, Default)]
111pub struct Report {
112 /// OIDs of objects that completed successfully.
113 pub succeeded: Vec<String>,
114 /// OIDs and reasons for objects that ultimately failed.
115 pub failed: Vec<(String, TransferError)>,
116}
117
118impl Report {
119 pub fn is_complete_success(&self) -> bool {
120 self.failed.is_empty()
121 }
122}