http.zig cleanup. Most notably, we now retry on gateway errors

This commit is contained in:
Emil Lerch 2026-03-10 14:37:36 -07:00
parent a7448525ed
commit feb1fe21f0
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -51,16 +51,26 @@ pub const Client = struct {
fn request(self: *Client, method: std.http.Method, url: []const u8, body: ?[]const u8, extra_headers: []const std.http.Header) HttpError!Response { fn request(self: *Client, method: std.http.Method, url: []const u8, body: ?[]const u8, extra_headers: []const std.http.Header) HttpError!Response {
var attempt: u8 = 0; var attempt: u8 = 0;
while (true) : (attempt += 1) { while (true) : (attempt += 1) {
if (self.doRequest(method, url, body, extra_headers)) |response| { const response = self.doRequest(method, url, body, extra_headers) catch {
return classifyResponse(response);
} else |_| {
if (attempt >= self.max_retries) return HttpError.RequestFailed; if (attempt >= self.max_retries) return HttpError.RequestFailed;
const backoff = self.base_backoff_ms * std.math.shl(u64, 1, attempt); self.backoffSleep(attempt);
std.Thread.sleep(backoff * std.time.ns_per_ms); continue;
} };
return classifyResponse(response) catch |err| {
if (err == HttpError.ServerError and attempt < self.max_retries) {
self.backoffSleep(attempt);
continue;
}
return err;
};
} }
} }
fn backoffSleep(self: *Client, attempt: u8) void {
const backoff = self.base_backoff_ms * std.math.shl(u64, 1, attempt);
std.Thread.sleep(backoff * std.time.ns_per_ms);
}
fn doRequest(self: *Client, method: std.http.Method, url: []const u8, body: ?[]const u8, extra_headers: []const std.http.Header) HttpError!Response { fn doRequest(self: *Client, method: std.http.Method, url: []const u8, body: ?[]const u8, extra_headers: []const std.http.Header) HttpError!Response {
var aw: std.Io.Writer.Allocating = .init(self.allocator); var aw: std.Io.Writer.Allocating = .init(self.allocator);
@ -88,18 +98,23 @@ pub const Client = struct {
} }
fn classifyResponse(response: Response) HttpError!Response { fn classifyResponse(response: Response) HttpError!Response {
return switch (response.status) { switch (response.status) {
.ok => response, .ok => return response,
.too_many_requests => HttpError.RateLimited, else => {
.unauthorized, .forbidden => HttpError.Unauthorized, response.allocator.free(response.body);
.not_found => HttpError.NotFound, return switch (response.status) {
.internal_server_error, .bad_gateway, .service_unavailable, .gateway_timeout => HttpError.ServerError, .too_many_requests => HttpError.RateLimited,
else => HttpError.InvalidResponse, .unauthorized, .forbidden => HttpError.Unauthorized,
}; .not_found => HttpError.NotFound,
.internal_server_error, .bad_gateway, .service_unavailable, .gateway_timeout => HttpError.ServerError,
else => HttpError.InvalidResponse,
};
},
}
} }
}; };
/// Build a URL with query parameters. /// Build a URL with query parameters. Values are percent-encoded per RFC 3986.
pub fn buildUrl( pub fn buildUrl(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
base: []const u8, base: []const u8,
@ -113,20 +128,26 @@ pub fn buildUrl(
try aw.writer.writeByte(if (i == 0) '?' else '&'); try aw.writer.writeByte(if (i == 0) '?' else '&');
try aw.writer.writeAll(param[0]); try aw.writer.writeAll(param[0]);
try aw.writer.writeByte('='); try aw.writer.writeByte('=');
for (param[1]) |c| { try std.Uri.Component.percentEncode(&aw.writer, param[1], isQueryValueChar);
switch (c) {
' ' => try aw.writer.writeAll("%20"),
'&' => try aw.writer.writeAll("%26"),
'=' => try aw.writer.writeAll("%3D"),
'+' => try aw.writer.writeAll("%2B"),
else => try aw.writer.writeByte(c),
}
}
} }
return aw.toOwnedSlice(); return aw.toOwnedSlice();
} }
/// RFC 3986 query-safe characters, excluding '&' and '=' which delimit
/// key=value pairs within the query string.
fn isQueryValueChar(c: u8) bool {
return switch (c) {
// Unreserved characters (RFC 3986 section 2.3)
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
// Sub-delimiters safe in query values (excludes '&' and '=')
'!', '$', '\'', '(', ')', '*', '+', ',', ';' => true,
// Additional query/path characters
':', '@', '/', '?' => true,
else => false,
};
}
test "buildUrl" { test "buildUrl" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const url = try buildUrl(allocator, "https://api.example.com/v1/data", &.{ const url = try buildUrl(allocator, "https://api.example.com/v1/data", &.{