http.zig cleanup. Most notably, we now retry on gateway errors
This commit is contained in:
parent
a7448525ed
commit
feb1fe21f0
1 changed files with 45 additions and 24 deletions
|
|
@ -51,15 +51,25 @@ 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;
|
||||||
|
self.backoffSleep(attempt);
|
||||||
|
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);
|
const backoff = self.base_backoff_ms * std.math.shl(u64, 1, attempt);
|
||||||
std.Thread.sleep(backoff * std.time.ns_per_ms);
|
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 {
|
||||||
|
switch (response.status) {
|
||||||
|
.ok => return response,
|
||||||
|
else => {
|
||||||
|
response.allocator.free(response.body);
|
||||||
return switch (response.status) {
|
return switch (response.status) {
|
||||||
.ok => response,
|
|
||||||
.too_many_requests => HttpError.RateLimited,
|
.too_many_requests => HttpError.RateLimited,
|
||||||
.unauthorized, .forbidden => HttpError.Unauthorized,
|
.unauthorized, .forbidden => HttpError.Unauthorized,
|
||||||
.not_found => HttpError.NotFound,
|
.not_found => HttpError.NotFound,
|
||||||
.internal_server_error, .bad_gateway, .service_unavailable, .gateway_timeout => HttpError.ServerError,
|
.internal_server_error, .bad_gateway, .service_unavailable, .gateway_timeout => HttpError.ServerError,
|
||||||
else => HttpError.InvalidResponse,
|
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", &.{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue