@@ -333,15 +333,23 @@ pub const Retry = struct {
333333 ///
334334 /// 0 means it should never retry.
335335 max_retries : u16 = 3 ,
336+ /// Normally, the retry delay is calculated with jitter, but some instances
337+ /// may require an override. When this value is used, it is set back to
338+ /// `null`.
339+ retry_delay_override_ms : ? u32 = null ,
336340 /// Hard cap of how many milliseconds to wait between retries.
337341 const MAX_RETRY_SLEEP_MS : u32 = 10 * 1000 ;
338342 /// Minimum time a jittered retry delay could be
339343 const MIN_RETRY_JITTER_MS : u32 = 500 ;
340344 /// Maximum time a jittered retry delay could be
341345 const MAX_RETRY_JITTER_MS : u32 = 1500 ;
342346
343- fn retryDelayMs (r : * Retry ) u32 {
344- var prng = std .Random .DefaultPrng .init (@as (u64 , @intCast (std .time .timestamp ())));
347+ fn calcRetryDelayMs (r : * Retry ) u32 {
348+ if (r .retry_delay_override_ms ) | delay | {
349+ r .retry_delay_override_ms = null ;
350+ return delay ;
351+ }
352+ var prng = std .Random .DefaultPrng .init (@as (u64 , @bitCast (std .time .timestamp ())));
345353 const rand = prng .random ();
346354 if (r .cur_retries == 0 ) {
347355 return rand .intRangeAtMost (u32 , MIN_RETRY_JITTER_MS , MAX_RETRY_JITTER_MS );
@@ -359,7 +367,7 @@ pub const Retry = struct {
359367 while (true ) {
360368 return @call (.auto , callback , args ) catch | err | {
361369 if (maybe_spurious (err ) and r .cur_retries < r .max_retries ) {
362- std .Thread .sleep (std .time .ns_per_ms * r .retryDelayMs ());
370+ std .Thread .sleep (std .time .ns_per_ms * r .calcRetryDelayMs ());
363371 r .cur_retries += 1 ;
364372 continue ;
365373 }
@@ -1126,6 +1134,13 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11261134 const status = response .head .status ;
11271135
11281136 if (@intFromEnum (status ) >= 500 or status == .too_many_requests or status == .not_found ) {
1137+ if (parseRetryAfter (response ) catch null ) | delay_sec | {
1138+ // Set max by dividing and multiplying again, because Retry-After
1139+ // header value needs to be u32, and could be obsurdly large, and
1140+ // we do not want to multiply that large number by 1000 in case of
1141+ // overflow. So we cap it first, then convert to milliseconds.
1142+ f .retry .retry_delay_override_ms = @min (delay_sec , Retry .MAX_RETRY_SLEEP_MS / 1000 ) * @as (u32 , 1000 );
1143+ }
11291144 return BadHttpStatus .MaybeSpurious ;
11301145 } else if (status != .ok ) {
11311146 return BadHttpStatus .NonSpurious ;
@@ -1134,6 +1149,94 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11341149 resource .http_request .decompress_buffer = try arena .alloc (u8 , response .head .content_encoding .minBufferCapacity ());
11351150}
11361151
1152+ /// Iterate through response headers to find Retry-After header, and parse the
1153+ /// value to return a value that represents how many seconds from now to wait
1154+ /// until a retry may be attempted.
1155+ ///
1156+ /// If the header does not exist, `null` is returned. If the header value is
1157+ /// invalid, an error is returned to indicate that parsing failed.
1158+ ///
1159+ /// Note that the implementation of this method in Cargo only respects this
1160+ /// header for 503 and 429 status codes, but the specification on MDN does not
1161+ /// restrict this header's usage to only those two status codes, so we won't
1162+ /// either.
1163+ ///
1164+ /// For more information, see the MDN documentation:
1165+ /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After
1166+ fn parseRetryAfter (response : * const std.http.Client.Response ) ! ? u32 {
1167+ var iter = response .head .iterateHeaders ();
1168+ const retry_after : []const u8 = while (iter .next ()) | header | {
1169+ if (ascii .eqlIgnoreCase (header .name , "retry-after" )) {
1170+ break header .value ;
1171+ }
1172+ } else {
1173+ return null ;
1174+ };
1175+
1176+ // Option 1: The value is a positive integer indicating number of seconds
1177+ // to wait before retrying.
1178+ if (std .fmt .parseInt (u32 , retry_after , 10 ) catch null ) | value | {
1179+ return value ;
1180+ }
1181+
1182+ // Option 2: The value is a timestamp that we need to parse, then use with
1183+ // current time to calculate how many seconds to wait before retrying.
1184+ //
1185+ // Parsing based on this specification:
1186+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date
1187+ const RETRY_AFTER_LEN : usize = 29 ;
1188+ if (retry_after .len != RETRY_AFTER_LEN ) {
1189+ return error .InvalidHeaderValueLength ;
1190+ }
1191+
1192+ // Much more memory compact than an array of string slices, because
1193+ // pointers are large. Also 12 strings means 11 more `\0` bytes than we
1194+ // actually need.
1195+ const months = "JanFebMarAprMayJunJulAugSepOctNovDec" ;
1196+
1197+ const epoch = std .time .epoch ;
1198+
1199+ const year = try std .fmt .parseInt (epoch .Year , retry_after [12.. 16], 10 );
1200+ const month : epoch.Month = for (0.. 12) | i | {
1201+ const month = months [i * 3 .. (i * 3 ) + 3 ];
1202+ if (std .mem .eql (u8 , month , retry_after [8.. 11])) {
1203+ break @enumFromInt (i + 1 );
1204+ }
1205+ } else {
1206+ return error .CannotFindMonth ;
1207+ };
1208+ const day = try std .fmt .parseInt (epoch .Day , retry_after [5.. 7], 10 );
1209+ const hour = try std .fmt .parseInt (epoch .Hour , retry_after [17.. 19], 10 );
1210+ const minute = try std .fmt .parseInt (epoch .Minute , retry_after [20.. 22], 10 );
1211+ const second = try std .fmt .parseInt (epoch .Second , retry_after [23.. 25], 10 );
1212+
1213+ const datetime = epoch.Datetime {
1214+ .year = year ,
1215+ .month = month ,
1216+ .day = day ,
1217+ .hour = hour ,
1218+ .minute = minute ,
1219+ .second = second ,
1220+ };
1221+ const datetime_epoch = try datetime .asEpochSeconds ();
1222+ const timestamp = std .time .timestamp ();
1223+
1224+ // We cannot support Retry-After if our system time is before epoch, but
1225+ // it is technically not an error, so return `null`.
1226+ if (timestamp < 0 ) {
1227+ return null ;
1228+ }
1229+
1230+ const now = @as (u64 , @intCast (std .time .timestamp ()));
1231+
1232+ // If Retry-After is before now, no delay is needed
1233+ if (datetime_epoch .secs <= now ) {
1234+ return 0 ;
1235+ }
1236+
1237+ return @as (u32 , @intCast (datetime_epoch .secs - now ));
1238+ }
1239+
11371240fn initGitResource (f : * Fetch , uri : std.Uri , resource : * Resource , reader_buffer : []u8 ) ! void {
11381241 const eb = & f .error_bundle ;
11391242
0 commit comments