@@ -333,14 +333,18 @@ 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 : ? 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 {
347+ fn calcRetryDelayMs (r : * Retry ) u32 {
344348 var prng = std .Random .DefaultPrng .init (@as (u64 , @intCast (std .time .timestamp ())));
345349 const rand = prng .random ();
346350 if (r .cur_retries == 0 ) {
@@ -359,7 +363,8 @@ pub const Retry = struct {
359363 while (true ) {
360364 return @call (.auto , callback , args ) catch | err | {
361365 if (maybe_spurious (err ) and r .cur_retries < r .max_retries ) {
362- std .Thread .sleep (std .time .ns_per_ms * r .retryDelayMs ());
366+ std .Thread .sleep (std .time .ns_per_ms * (r .retry_delay_override orelse r .calcRetryDelayMs ()));
367+ r .retry_delay_override = null ;
363368 r .cur_retries += 1 ;
364369 continue ;
365370 }
@@ -1126,6 +1131,9 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11261131 const status = response .head .status ;
11271132
11281133 if (@intFromEnum (status ) >= 500 or status == .too_many_requests or status == .not_found ) {
1134+ if (parseRetryAfter (response ) catch null ) | delay | {
1135+ f .retry .retry_delay_override = delay ;
1136+ }
11291137 return BadHttpStatus .MaybeSpurious ;
11301138 } else if (status != .ok ) {
11311139 return BadHttpStatus .NonSpurious ;
@@ -1134,6 +1142,94 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11341142 resource .http_request .decompress_buffer = try arena .alloc (u8 , response .head .content_encoding .minBufferCapacity ());
11351143}
11361144
1145+ /// Iterate through response headers to find Retry-After header, and parse the
1146+ /// value to return a value that represents how many seconds from now to wait
1147+ /// until a retry may be attempted.
1148+ ///
1149+ /// If the header does not exist, `null` is returned. If the header value is
1150+ /// invalid, an error is returned to indicate that parsing failed.
1151+ ///
1152+ /// Note that the implementation of this method in Cargo only respects this
1153+ /// header for 503 and 429 status codes, but the specification on MDN does not
1154+ /// restrict this header's usage to only those two status codes, so we won't
1155+ /// either.
1156+ ///
1157+ /// For more information, see the MDN documentation:
1158+ /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After
1159+ fn parseRetryAfter (response : * const std.http.Client.Response ) ! ? u32 {
1160+ var iter = response .head .iterateHeaders ();
1161+ const retry_after : []const u8 = while (iter .next ()) | header | {
1162+ if (ascii .eqlIgnoreCase (header .name , "retry-after" )) {
1163+ break header .value ;
1164+ }
1165+ } else {
1166+ return null ;
1167+ };
1168+
1169+ // Option 1: The value is a positive integer indicating number of seconds
1170+ // to wait before retrying.
1171+ if (std .fmt .parseInt (u32 , retry_after , 10 ) catch null ) | value | {
1172+ return value ;
1173+ }
1174+
1175+ // Option 2: The value is a timestamp that we need to parse, then use with
1176+ // current time to calculate how many seconds to wait before retrying.
1177+ //
1178+ // Parsing based on this specification:
1179+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date
1180+ const RETRY_AFTER_LEN : usize = 29 ;
1181+ if (retry_after .len != RETRY_AFTER_LEN ) {
1182+ return error .InvalidHeaderValueLength ;
1183+ }
1184+
1185+ // Much more memory compact than an array of string slices, because
1186+ // pointers are large. Also 12 strings means 11 more `\0` bytes than we
1187+ // actually need.
1188+ const months = "JanFebMarAprMayJunJulAugSepOctNovDec" ;
1189+
1190+ const epoch = std .time .epoch ;
1191+
1192+ const year = try std .fmt .parseInt (epoch .Year , retry_after [12.. 16], 10 );
1193+ const month : epoch.Month = for (0.. 12) | i | {
1194+ const month = months [i * 3 .. (i * 3 ) + 3 ];
1195+ if (std .mem .eql (u8 , month , retry_after [8.. 11])) {
1196+ break @enumFromInt (i + 1 );
1197+ }
1198+ } else {
1199+ return error .CannotFindMonth ;
1200+ };
1201+ const day = try std .fmt .parseInt (epoch .Day , retry_after [5.. 7], 10 );
1202+ const hour = try std .fmt .parseInt (epoch .Hour , retry_after [17.. 19], 10 );
1203+ const minute = try std .fmt .parseInt (epoch .Minute , retry_after [20.. 22], 10 );
1204+ const second = try std .fmt .parseInt (epoch .Second , retry_after [23.. 25], 10 );
1205+
1206+ const datetime = epoch.Datetime {
1207+ .year = year ,
1208+ .month = month ,
1209+ .day = day ,
1210+ .hour = hour ,
1211+ .minute = minute ,
1212+ .second = second ,
1213+ };
1214+ const datetime_epoch = try datetime .asEpochSeconds ();
1215+ const timestamp = std .time .timestamp ();
1216+
1217+ // We cannot support Retry-After if our system time is before epoch, but
1218+ // it is technically not an error, so return `null`.
1219+ if (timestamp < 0 ) {
1220+ return null ;
1221+ }
1222+
1223+ const now = @as (u64 , @intCast (std .time .timestamp ()));
1224+
1225+ // If Retry-After is before now, no delay is needed
1226+ if (datetime_epoch .secs <= now ) {
1227+ return 0 ;
1228+ }
1229+
1230+ return @as (u32 , @intCast (datetime_epoch .secs - now ));
1231+ }
1232+
11371233fn initGitResource (f : * Fetch , uri : std.Uri , resource : * Resource , reader_buffer : []u8 ) ! void {
11381234 const eb = & f .error_bundle ;
11391235
0 commit comments