@@ -76,8 +76,13 @@ impl DownloadError {
7676 Self :: from_http_status ( status)
7777 }
7878
79- /// Create a DownloadError from a reqwest::Error
79+ /// Create a DownloadError from a reqwest::Error (owned)
8080 pub fn from_reqwest ( error : reqwest:: Error ) -> Self {
81+ Self :: from_reqwest_ref ( & error)
82+ }
83+
84+ /// Create a DownloadError from a reqwest::Error reference
85+ pub fn from_reqwest_ref ( error : & reqwest:: Error ) -> Self {
8186 if error. is_status ( ) {
8287 if let Some ( status) = error. status ( ) {
8388 return Self :: from_http_status ( status) ;
@@ -93,22 +98,17 @@ impl DownloadError {
9398 let error_str = error. to_string ( ) ;
9499
95100 // Check error source chain for TLS/SSL/Certificate errors
96- // We use a hybrid approach: check both type names and error messages
97- // - Type names catch concrete types before trait object erasure
98- // - Error messages catch issues when types are erased to dyn Error
99- let mut current_error: Option < & dyn std:: error:: Error > = Some ( & error) ;
101+ let mut current_error: Option < & dyn std:: error:: Error > = Some ( error) ;
100102 let mut is_tls_error = false ;
101103
102104 while let Some ( err) = current_error {
103105 let error_msg = err. to_string ( ) . to_lowercase ( ) ;
104106
105- // Check error message for TLS-related keywords
106- // This works even when types are erased to trait objects
107107 let message_indicates_tls = error_msg. contains ( "certificate" )
108108 || error_msg. contains ( "tls" )
109109 || error_msg. contains ( "ssl" )
110- || error_msg. contains ( "trust setting" ) // macOS Security Framework
111- || error_msg. contains ( "trust policy" ) ; // macOS Security Framework
110+ || error_msg. contains ( "trust setting" )
111+ || error_msg. contains ( "trust policy" ) ;
112112
113113 if message_indicates_tls {
114114 is_tls_error = true ;
@@ -122,14 +122,12 @@ impl DownloadError {
122122 return DownloadError :: TlsError ( error_str) ;
123123 }
124124
125- // Check for DNS errors
126125 if error_str. contains ( "dns" ) || error_str. contains ( "failed to lookup address" ) {
127126 return DownloadError :: DnsError ( error_str) ;
128127 }
129128 return DownloadError :: ConnectionError ( error_str) ;
130129 }
131130
132- // Fallback for other error types
133131 DownloadError :: Other ( error. to_string ( ) )
134132 }
135133
@@ -231,6 +229,42 @@ impl fmt::Display for DownloadError {
231229
232230impl std:: error:: Error for DownloadError { }
233231
232+ /// Shared retry handler for download errors.
233+ ///
234+ /// Returns `Some(Duration)` with the delay to wait before retrying, or `None` if
235+ /// the error is non-retryable or max retries have been exceeded.
236+ pub fn handle_download_retry (
237+ error : & DownloadError ,
238+ retry_count : & mut usize ,
239+ max_retries : usize ,
240+ default_retry_delay_secs : u64 ,
241+ ) -> Option < Duration > {
242+ if !error. is_retryable ( ) {
243+ eprintln ! (
244+ "\n Download failed with non-retryable error: {}" ,
245+ error. format_error( )
246+ ) ;
247+ return None ;
248+ }
249+ if * retry_count >= max_retries {
250+ eprintln ! ( "\n Max retries ({}) reached, giving up" , max_retries) ;
251+ eprintln ! ( "Last error: {}" , error. format_error( ) ) ;
252+ return None ;
253+ }
254+ let retry_delay = error
255+ . suggested_retry_delay ( )
256+ . unwrap_or_else ( || Duration :: from_secs ( default_retry_delay_secs) ) ;
257+ eprintln ! ( "\n Download failed: {}" , error. format_error( ) ) ;
258+ eprintln ! (
259+ "Retrying in {} seconds... (attempt {}/{})" ,
260+ retry_delay. as_secs( ) ,
261+ * retry_count + 1 ,
262+ max_retries
263+ ) ;
264+ * retry_count += 1 ;
265+ Some ( retry_delay)
266+ }
267+
234268#[ cfg( test) ]
235269mod tests {
236270 use super :: * ;
0 commit comments