@@ -47,6 +47,7 @@ use anyhow::Error;
47
47
use rand:: Rng ;
48
48
use std:: cmp:: min;
49
49
use std:: time:: Duration ;
50
+ use time:: OffsetDateTime ;
50
51
51
52
/// State for managing retrying a network operation.
52
53
pub struct Retry < ' a > {
@@ -104,8 +105,8 @@ impl<'a> Retry<'a> {
104
105
pub fn r#try < T > ( & mut self , f : impl FnOnce ( ) -> CargoResult < T > ) -> RetryResult < T > {
105
106
match f ( ) {
106
107
Err ( ref e) if maybe_spurious ( e) && self . retries < self . max_retries => {
107
- let err_msg = e
108
- . downcast_ref :: < HttpNotSuccessful > ( )
108
+ let err = e. downcast_ref :: < HttpNotSuccessful > ( ) ;
109
+ let err_msg = err
109
110
. map ( |http_err| http_err. display_short ( ) )
110
111
. unwrap_or_else ( || e. root_cause ( ) . to_string ( ) ) ;
111
112
let left_retries = self . max_retries - self . retries ;
@@ -118,7 +119,12 @@ impl<'a> Retry<'a> {
118
119
return RetryResult :: Err ( e) ;
119
120
}
120
121
self . retries += 1 ;
121
- RetryResult :: Retry ( self . next_sleep_ms ( ) )
122
+ let sleep = err
123
+ . and_then ( Self :: parse_retry_after)
124
+ // Limit the Retry-After to a maximum value to avoid waiting too long.
125
+ . map ( |retry_after| retry_after. min ( MAX_RETRY_SLEEP_MS ) )
126
+ . unwrap_or_else ( || self . next_sleep_ms ( ) ) ;
127
+ RetryResult :: Retry ( sleep)
122
128
}
123
129
Err ( e) => RetryResult :: Err ( e) ,
124
130
Ok ( r) => RetryResult :: Success ( r) ,
@@ -141,6 +147,40 @@ impl<'a> Retry<'a> {
141
147
)
142
148
}
143
149
}
150
+
151
+ /// Parse the HTTP `Retry-After` header.
152
+ /// Returns the number of milliseconds to wait before retrying according to the header.
153
+ fn parse_retry_after ( response : & HttpNotSuccessful ) -> Option < u64 > {
154
+ // Only applies to HTTP 429 (too many requests) and 503 (service unavailable).
155
+ if !matches ! ( response. code, 429 | 503 ) {
156
+ return None ;
157
+ }
158
+
159
+ // Extract the Retry-After header value.
160
+ let retry_after = response
161
+ . headers
162
+ . iter ( )
163
+ . filter_map ( |h| h. split_once ( ':' ) )
164
+ . map ( |( k, v) | ( k. trim ( ) , v. trim ( ) ) )
165
+ . find ( |( k, _) | k. eq_ignore_ascii_case ( "retry-after" ) ) ?
166
+ . 1 ;
167
+
168
+ // First option: Retry-After is a positive integer of seconds to wait.
169
+ if let Ok ( delay_secs) = retry_after. parse :: < u32 > ( ) {
170
+ return Some ( delay_secs as u64 * 1000 ) ;
171
+ }
172
+
173
+ // Second option: Retry-After is a future HTTP date string that tells us when to retry.
174
+ if let Ok ( retry_time) =
175
+ OffsetDateTime :: parse ( retry_after, & time:: format_description:: well_known:: Rfc2822 )
176
+ {
177
+ let now = OffsetDateTime :: now_utc ( ) ;
178
+ if retry_time > now {
179
+ return Some ( ( retry_time - now) . whole_milliseconds ( ) as u64 ) ;
180
+ }
181
+ }
182
+ None
183
+ }
144
184
}
145
185
146
186
fn maybe_spurious ( err : & Error ) -> bool {
@@ -169,7 +209,7 @@ fn maybe_spurious(err: &Error) -> bool {
169
209
}
170
210
}
171
211
if let Some ( not_200) = err. downcast_ref :: < HttpNotSuccessful > ( ) {
172
- if 500 <= not_200. code && not_200. code < 600 {
212
+ if 500 <= not_200. code && not_200. code < 600 || not_200 . code == 429 {
173
213
return true ;
174
214
}
175
215
}
@@ -317,3 +357,51 @@ fn curle_http2_stream_is_spurious() {
317
357
let err = curl:: Error :: new ( code) ;
318
358
assert ! ( maybe_spurious( & err. into( ) ) ) ;
319
359
}
360
+
361
+ #[ test]
362
+ fn retry_after_parsing ( ) {
363
+ use crate :: core:: Shell ;
364
+ fn spurious ( code : u32 , header : & str ) -> HttpNotSuccessful {
365
+ HttpNotSuccessful {
366
+ code,
367
+ url : "Uri" . to_string ( ) ,
368
+ ip : None ,
369
+ body : Vec :: new ( ) ,
370
+ headers : vec ! [ header. to_string( ) ] ,
371
+ }
372
+ }
373
+
374
+ let headers = spurious ( 429 , "Retry-After: 10" ) ;
375
+ assert_eq ! ( Retry :: parse_retry_after( & headers) , Some ( 10_000 ) ) ;
376
+
377
+ let headers = spurious ( 429 , "retry-after: Fri, 01 Jan 2100 00:00:00 GMT" ) ;
378
+ let expected = ( OffsetDateTime :: new_utc (
379
+ time:: Date :: from_calendar_date ( 2100 , time:: Month :: January , 1 ) . unwrap ( ) ,
380
+ time:: Time :: from_hms ( 0 , 0 , 0 ) . unwrap ( ) ,
381
+ ) - OffsetDateTime :: now_utc ( ) )
382
+ . whole_milliseconds ( ) ;
383
+ let actual = Retry :: parse_retry_after ( & headers) . unwrap ( ) ;
384
+ assert ! ( ( expected. abs_diff( actual. into( ) ) < 1000 ) ) ;
385
+
386
+ let headers = spurious ( 429 , "Content-Type: text/html" ) ;
387
+ assert_eq ! ( Retry :: parse_retry_after( & headers) , None ) ;
388
+
389
+ let headers = spurious ( 429 , "retry-after: Fri, 01 Jan 2000 00:00:00 GMT" ) ;
390
+ assert_eq ! ( Retry :: parse_retry_after( & headers) , None ) ;
391
+
392
+ let headers = spurious ( 429 , "retry-after: -1" ) ;
393
+ assert_eq ! ( Retry :: parse_retry_after( & headers) , None ) ;
394
+
395
+ let headers = spurious ( 400 , "retry-after: 1" ) ;
396
+ assert_eq ! ( Retry :: parse_retry_after( & headers) , None ) ;
397
+
398
+ let gctx = GlobalContext :: default ( ) . unwrap ( ) ;
399
+ * gctx. shell ( ) = Shell :: from_write ( Box :: new ( Vec :: new ( ) ) ) ;
400
+ let mut retry = Retry :: new ( & gctx) . unwrap ( ) ;
401
+ match retry
402
+ . r#try ( || -> CargoResult < ( ) > { Err ( anyhow:: Error :: from ( spurious ( 429 , "Retry-After: 7" ) ) ) } )
403
+ {
404
+ RetryResult :: Retry ( sleep) => assert_eq ! ( sleep, 7_000 ) ,
405
+ _ => panic ! ( "unexpected non-retry" ) ,
406
+ }
407
+ }
0 commit comments