@@ -336,19 +336,26 @@ pub const Retry = struct {
336336 ///
337337 /// 0 means it should never retry.
338338 max_retries : u16 = 3 ,
339+ /// Normally, the retry delay is calculated with jitter, but some instances
340+ /// may require an override. When this value is used, it is set back to
341+ /// `null`.
342+ retry_delay_override_ms : ? u32 = null ,
339343 /// Hard cap of how many milliseconds to wait between retries.
340344 const MAX_RETRY_SLEEP_MS = 10 * 1000 ;
341345 /// Minimum time a jittered retry delay could be
342346 const MIN_RETRY_JITTER_MS = 500 ;
343347 /// Maximum time a jittered retry delay could be
344348 const MAX_RETRY_JITTER_MS = 1500 ;
345349
346- fn retryDelayMs (r : * Retry ) i64 {
350+ fn calcRetryDelayMs (r : * Retry ) i64 {
351+ if (r .retry_delay_override_ms ) | delay | {
352+ r .retry_delay_override_ms = null ;
353+ return delay ;
354+ }
347355 if (r .cur_retries == 0 ) {
348356 return std .crypto .random .intRangeAtMost (i64 , MIN_RETRY_JITTER_MS , MAX_RETRY_JITTER_MS );
349- } else {
350- return @min (r .cur_retries * 3 * 1000 , MAX_RETRY_SLEEP_MS );
351357 }
358+ return @min (r .cur_retries * 3 * 1000 , MAX_RETRY_SLEEP_MS );
352359 }
353360
354361 fn callWithRetries (
@@ -361,7 +368,7 @@ pub const Retry = struct {
361368 while (true ) {
362369 return @call (.auto , callback , args ) catch | err | {
363370 if (maybeSpurious (err ) and r .cur_retries < r .max_retries ) {
364- const delay = Io .Duration .fromMilliseconds (r .retryDelayMs ());
371+ const delay = Io .Duration .fromMilliseconds (r .calcRetryDelayMs ());
365372 try io .sleep (delay , .awake );
366373 r .cur_retries += 1 ;
367374 continue ;
@@ -1134,6 +1141,13 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11341141 const status = response .head .status ;
11351142
11361143 if (@intFromEnum (status ) >= 500 or status == .too_many_requests or status == .not_found ) {
1144+ if (parseRetryAfter (f .io , response ) catch null ) | delay_sec | {
1145+ // Set max by dividing and multiplying again, because Retry-After
1146+ // header value needs to be u32, and could be obsurdly large, and
1147+ // we do not want to multiply that large number by 1000 in case of
1148+ // overflow. So we cap it first, then convert to milliseconds.
1149+ f .retry .retry_delay_override_ms = @min (delay_sec , Retry .MAX_RETRY_SLEEP_MS / 1000 ) * @as (u32 , 1000 );
1150+ }
11371151 return BadHttpStatus .MaybeSpurious ;
11381152 } else if (status != .ok ) {
11391153 return BadHttpStatus .NonSpurious ;
@@ -1142,6 +1156,96 @@ fn initHttpResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer:
11421156 resource .http_request .decompress_buffer = try arena .alloc (u8 , response .head .content_encoding .minBufferCapacity ());
11431157}
11441158
1159+ /// Iterate through response headers to find Retry-After header, and parse the
1160+ /// value to return a value that represents how many seconds from now to wait
1161+ /// until a retry may be attempted.
1162+ ///
1163+ /// If the header does not exist, `null` is returned. If the header value is
1164+ /// invalid, an error is returned to indicate that parsing failed.
1165+ ///
1166+ /// Note that the implementation of this method in Cargo only respects this
1167+ /// header for 503 and 429 status codes, but the specification on MDN does not
1168+ /// restrict this header's usage to only those two status codes, so we won't
1169+ /// either.
1170+ ///
1171+ /// For more information, see the MDN documentation:
1172+ /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After
1173+ fn parseRetryAfter (io : Io , response : * const std.http.Client.Response ) ! ? u32 {
1174+ var iter = response .head .iterateHeaders ();
1175+ const retry_after : []const u8 = while (iter .next ()) | header | {
1176+ if (ascii .eqlIgnoreCase (header .name , "retry-after" )) {
1177+ break header .value ;
1178+ }
1179+ } else {
1180+ return null ;
1181+ };
1182+
1183+ // Option 1: The value is a positive integer indicating number of seconds
1184+ // to wait before retrying.
1185+ if (std .fmt .parseInt (u32 , retry_after , 10 ) catch null ) | value | {
1186+ return value ;
1187+ }
1188+
1189+ // Option 2: The value is a timestamp that we need to parse, then use with
1190+ // current time to calculate how many seconds to wait before retrying.
1191+ //
1192+ // Parsing based on this specification:
1193+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date
1194+ const RETRY_AFTER_LEN : usize = 29 ;
1195+ if (retry_after .len != RETRY_AFTER_LEN ) {
1196+ return error .InvalidHeaderValueLength ;
1197+ }
1198+
1199+ // Much more memory compact than an array of string slices, because
1200+ // pointers are large. Also 12 strings means 11 more `\0` bytes than we
1201+ // actually need.
1202+ const months = "JanFebMarAprMayJunJulAugSepOctNovDec" ;
1203+
1204+ const epoch = std .time .epoch ;
1205+
1206+ // Example date with periodic indices for easy visualization:
1207+ // Tue, 29 Oct 2024 16:56:32 GMT
1208+ // ^ ^ ^
1209+ // 0 10 20
1210+ const year = try std .fmt .parseInt (epoch .Year , retry_after [12.. 16], 10 );
1211+ const month : epoch.Month = for (0.. 12) | i | {
1212+ const month = months [i * 3 .. (i * 3 ) + 3 ];
1213+ if (std .mem .eql (u8 , month , retry_after [8.. 11])) {
1214+ break @enumFromInt (i + 1 );
1215+ }
1216+ } else {
1217+ return error .CannotFindMonth ;
1218+ };
1219+ const day = try std .fmt .parseInt (epoch .Day , retry_after [5.. 7], 10 );
1220+ const hour = try std .fmt .parseInt (epoch .Hour , retry_after [17.. 19], 10 );
1221+ const minute = try std .fmt .parseInt (epoch .Minute , retry_after [20.. 22], 10 );
1222+ const second = try std .fmt .parseInt (epoch .Second , retry_after [23.. 25], 10 );
1223+
1224+ const datetime = epoch.Datetime {
1225+ .year = year ,
1226+ .month = month ,
1227+ .day = day ,
1228+ .hour = hour ,
1229+ .minute = minute ,
1230+ .second = second ,
1231+ };
1232+ const timestamp_retry_after : Io.Timestamp = try datetime .asTimestamp ();
1233+ const timestamp_cur = try Io .Clock .Timestamp .now (io , .real );
1234+
1235+ // If Retry-After is before or equal to now, disregard it and calc delay as usual
1236+ if (timestamp_retry_after .nanoseconds <= timestamp_cur .raw .nanoseconds ) {
1237+ return null ;
1238+ }
1239+
1240+ // This is guaranteed to be positive, so represent as u48
1241+ const diff : u48 = @intCast (timestamp_retry_after .nanoseconds - timestamp_cur .raw .nanoseconds );
1242+
1243+ // If we get a number too large to fit into u32, it's way too big anyway, we can cap at u32's max
1244+ const capped : u32 = @min (diff , 2 ** 32 );
1245+
1246+ return capped ;
1247+ }
1248+
11451249fn initGitResource (f : * Fetch , uri : std.Uri , resource : * Resource , reader_buffer : []u8 ) ! void {
11461250 const eb = & f .error_bundle ;
11471251
0 commit comments