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