@@ -333,21 +333,28 @@ 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 ())));
345- const rand = prng .random ();
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+ }
346352 if (r .cur_retries == 0 ) {
353+ var prng = std .Random .DefaultPrng .init (@as (u64 , @bitCast (std .time .microTimestamp ())));
354+ const rand = prng .random ();
347355 return rand .intRangeAtMost (u32 , MIN_RETRY_JITTER_MS , MAX_RETRY_JITTER_MS );
348- } else {
349- return @min (r .cur_retries * 3 * 1000 , MAX_RETRY_SLEEP_MS );
350356 }
357+ return @min (r .cur_retries * 3 * 1000 , MAX_RETRY_SLEEP_MS );
351358 }
352359
353360 fn callWithRetries (
@@ -358,8 +365,8 @@ pub const Retry = struct {
358365 ) @typeInfo (FunctionType ).@"fn" .return_type .? {
359366 while (true ) {
360367 return @call (.auto , callback , args ) catch | err | {
361- if (maybe_spurious (err ) and r .cur_retries < r .max_retries ) {
362- std .Thread .sleep (std .time .ns_per_ms * r . retryDelayMs ( ));
368+ if (maybeSpurious (err ) and r .cur_retries < r .max_retries ) {
369+ std .Thread .sleep (std .time .ns_per_ms * @as ( u64 , r . calcRetryDelayMs () ));
363370 r .cur_retries += 1 ;
364371 continue ;
365372 }
@@ -374,7 +381,7 @@ const BadHttpStatus = error{
374381 NonSpurious ,
375382};
376383
377- fn maybe_spurious (err : anyerror ) bool {
384+ fn maybeSpurious (err : anyerror ) bool {
378385 return switch (err ) {
379386 // tcp errors
380387 std .http .Client .ConnectTcpError .ConnectionTimedOut ,
@@ -1126,6 +1133,13 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11261133 const status = response .head .status ;
11271134
11281135 if (@intFromEnum (status ) >= 500 or status == .too_many_requests or status == .not_found ) {
1136+ if (parseRetryAfter (response ) catch null ) | delay_sec | {
1137+ // Set max by dividing and multiplying again, because Retry-After
1138+ // header value needs to be u32, and could be obsurdly large, and
1139+ // we do not want to multiply that large number by 1000 in case of
1140+ // overflow. So we cap it first, then convert to milliseconds.
1141+ f .retry .retry_delay_override_ms = @min (delay_sec , Retry .MAX_RETRY_SLEEP_MS / 1000 ) * @as (u32 , 1000 );
1142+ }
11291143 return BadHttpStatus .MaybeSpurious ;
11301144 } else if (status != .ok ) {
11311145 return BadHttpStatus .NonSpurious ;
@@ -1134,6 +1148,94 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11341148 resource .http_request .decompress_buffer = try arena .alloc (u8 , response .head .content_encoding .minBufferCapacity ());
11351149}
11361150
1151+ /// Iterate through response headers to find Retry-After header, and parse the
1152+ /// value to return a value that represents how many seconds from now to wait
1153+ /// until a retry may be attempted.
1154+ ///
1155+ /// If the header does not exist, `null` is returned. If the header value is
1156+ /// invalid, an error is returned to indicate that parsing failed.
1157+ ///
1158+ /// Note that the implementation of this method in Cargo only respects this
1159+ /// header for 503 and 429 status codes, but the specification on MDN does not
1160+ /// restrict this header's usage to only those two status codes, so we won't
1161+ /// either.
1162+ ///
1163+ /// For more information, see the MDN documentation:
1164+ /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After
1165+ fn parseRetryAfter (response : * const std.http.Client.Response ) ! ? u32 {
1166+ var iter = response .head .iterateHeaders ();
1167+ const retry_after : []const u8 = while (iter .next ()) | header | {
1168+ if (ascii .eqlIgnoreCase (header .name , "retry-after" )) {
1169+ break header .value ;
1170+ }
1171+ } else {
1172+ return null ;
1173+ };
1174+
1175+ // Option 1: The value is a positive integer indicating number of seconds
1176+ // to wait before retrying.
1177+ if (std .fmt .parseInt (u32 , retry_after , 10 ) catch null ) | value | {
1178+ return value ;
1179+ }
1180+
1181+ // Option 2: The value is a timestamp that we need to parse, then use with
1182+ // current time to calculate how many seconds to wait before retrying.
1183+ //
1184+ // Parsing based on this specification:
1185+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date
1186+ const RETRY_AFTER_LEN : usize = 29 ;
1187+ if (retry_after .len != RETRY_AFTER_LEN ) {
1188+ return error .InvalidHeaderValueLength ;
1189+ }
1190+
1191+ // Much more memory compact than an array of string slices, because
1192+ // pointers are large. Also 12 strings means 11 more `\0` bytes than we
1193+ // actually need.
1194+ const months = "JanFebMarAprMayJunJulAugSepOctNovDec" ;
1195+
1196+ const epoch = std .time .epoch ;
1197+
1198+ const year = try std .fmt .parseInt (epoch .Year , retry_after [12.. 16], 10 );
1199+ const month : epoch.Month = for (0.. 12) | i | {
1200+ const month = months [i * 3 .. (i * 3 ) + 3 ];
1201+ if (std .mem .eql (u8 , month , retry_after [8.. 11])) {
1202+ break @enumFromInt (i + 1 );
1203+ }
1204+ } else {
1205+ return error .CannotFindMonth ;
1206+ };
1207+ const day = try std .fmt .parseInt (epoch .Day , retry_after [5.. 7], 10 );
1208+ const hour = try std .fmt .parseInt (epoch .Hour , retry_after [17.. 19], 10 );
1209+ const minute = try std .fmt .parseInt (epoch .Minute , retry_after [20.. 22], 10 );
1210+ const second = try std .fmt .parseInt (epoch .Second , retry_after [23.. 25], 10 );
1211+
1212+ const datetime = epoch.Datetime {
1213+ .year = year ,
1214+ .month = month ,
1215+ .day = day ,
1216+ .hour = hour ,
1217+ .minute = minute ,
1218+ .second = second ,
1219+ };
1220+ const datetime_epoch = try datetime .asEpochSeconds ();
1221+ const timestamp = std .time .timestamp ();
1222+
1223+ // We cannot support Retry-After if our system time is before epoch, but
1224+ // it is technically not an error, so return `null`.
1225+ if (timestamp < 0 ) {
1226+ return null ;
1227+ }
1228+
1229+ const now = @as (u64 , @intCast (std .time .timestamp ()));
1230+
1231+ // If Retry-After is before now, no delay is needed
1232+ if (datetime_epoch .secs <= now ) {
1233+ return 0 ;
1234+ }
1235+
1236+ return @as (u32 , @intCast (datetime_epoch .secs - now ));
1237+ }
1238+
11371239fn initGitResource (f : * Fetch , uri : std.Uri , resource : * Resource , reader_buffer : []u8 ) ! void {
11381240 const eb = & f .error_bundle ;
11391241
0 commit comments