1use std::time::Duration;
4
5#[derive(Debug, thiserror::Error)]
6pub enum TransferError {
7 #[error("network error: {0}")]
9 Network(String),
10
11 #[error("timed out after {0:?} with no data")]
13 Timeout(Duration),
14
15 #[error("server returned HTTP {status}")]
17 Status {
18 status: u16,
19 retry_after: Option<Duration>,
20 },
21
22 #[error("remote resource changed: {0}")]
25 RemoteChanged(String),
26
27 #[error("protocol violation: {0}")]
31 Protocol(String),
32
33 #[error("write failed: {0}")]
35 Io(String),
36
37 #[error("cancelled")]
39 Cancelled,
40}
41
42impl TransferError {
43 pub fn is_retryable(&self) -> bool {
46 match self {
47 TransferError::Network(_) | TransferError::Timeout(_) => true,
48 TransferError::Status { status, .. } => {
49 *status == 408 || *status == 429 || (500..600).contains(status)
50 }
51 TransferError::Protocol(_)
55 | TransferError::RemoteChanged(_)
56 | TransferError::Io(_)
57 | TransferError::Cancelled => false,
58 }
59 }
60
61 pub fn retry_after(&self) -> Option<Duration> {
63 match self {
64 TransferError::Status { retry_after, .. } => *retry_after,
65 _ => None,
66 }
67 }
68
69 pub fn from_reqwest(err: &reqwest::Error) -> Self {
70 if err.is_timeout() {
74 TransferError::Timeout(Duration::ZERO)
75 } else {
76 TransferError::Network(sanitize_reqwest(err))
77 }
78 }
79}
80
81fn sanitize_reqwest(err: &reqwest::Error) -> String {
84 let mut msg = err.to_string();
85 if let Some(url) = err.url() {
86 let redacted = crate::fmt::short_url(url.as_str());
87 msg = msg.replace(url.as_str(), &redacted);
88 }
89 let mut source = std::error::Error::source(err);
91 let mut depth = 0;
92 while let Some(s) = source {
93 if depth >= 3 {
94 break;
95 }
96 msg.push_str(": ");
97 msg.push_str(&s.to_string());
98 source = s.source();
99 depth += 1;
100 }
101 msg
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn retryable_classification() {
110 assert!(TransferError::Network("reset".into()).is_retryable());
111 assert!(TransferError::Timeout(Duration::from_secs(1)).is_retryable());
112 for s in [408, 429, 500, 502, 503, 504] {
113 assert!(
114 TransferError::Status {
115 status: s,
116 retry_after: None
117 }
118 .is_retryable(),
119 "{s} should retry"
120 );
121 }
122 for s in [400, 401, 403, 404, 416] {
123 assert!(
124 !TransferError::Status {
125 status: s,
126 retry_after: None
127 }
128 .is_retryable(),
129 "{s} should not retry"
130 );
131 }
132 assert!(!TransferError::RemoteChanged("etag".into()).is_retryable());
133 assert!(!TransferError::Protocol("ignored range".into()).is_retryable());
134 assert!(!TransferError::Cancelled.is_retryable());
135 }
136}