Skip to content

Commit 2ad8daa

Browse files
committed
Implement the use of Retry-After HTTP header to set retry delay
1 parent b409e41 commit 2ad8daa

File tree

1 file changed

+98
-2
lines changed

1 file changed

+98
-2
lines changed

src/Package/Fetch.zig

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
11371233
fn initGitResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u8) !void {
11381234
const eb = &f.error_bundle;
11391235

0 commit comments

Comments
 (0)