forked from jstrieb/github-stats
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttp_client.zig
More file actions
119 lines (110 loc) · 3.62 KB
/
http_client.zig
File metadata and controls
119 lines (110 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Naive, unoptimized HTTP client with a .request method that wraps Zig's HTTP
//! client fetch. Simple, and not particularly efficient. Response bodies stay
//! allocated for the lifetime of the client.
const std = @import("std");
allocator: std.mem.Allocator,
io: std.Io,
client: std.http.Client,
bearer: []const u8,
token: []const u8,
const Self = @This();
const Response = struct {
body: []const u8,
status: std.http.Status,
};
const Request = struct {
url: []const u8,
body: ?[]const u8 = null,
headers: std.http.Client.Request.Headers = .{},
extra_headers: []const std.http.Header = &.{},
};
pub fn init(allocator: std.mem.Allocator, io: std.Io, token: []const u8) !Self {
const bearer = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token});
errdefer allocator.free(bearer);
const cloned_token = try allocator.dupe(u8, token);
errdefer allocator.free(cloned_token);
return .{
.allocator = allocator,
.io = io,
.client = .{ .allocator = allocator, .io = io },
.bearer = bearer,
.token = cloned_token,
};
}
pub fn deinit(self: *Self) void {
self.client.deinit();
self.allocator.free(self.bearer);
self.allocator.free(self.token);
}
pub fn fetch(self: *Self, request: Request, retries: isize) !Response {
if (retries <= -1) {
return error.TooManyRetries;
}
var writer =
try std.Io.Writer.Allocating.initCapacity(self.allocator, 1024);
var writer_initialized = true;
errdefer if (writer_initialized) writer.deinit();
const status = (self.client.fetch(.{
.location = .{ .url = request.url },
.response_writer = &writer.writer,
.payload = request.body,
.headers = request.headers,
.extra_headers = request.extra_headers,
}) catch |err| switch (err) {
error.HttpConnectionClosing => {
// Handle a Zig HTTP bug where keep-alive connections are closed by
// the server after a timeout, but the client doesn't handle it
// properly. For now we nuke the whole client (and associated
// connection pool) and make a new one, but there might be a better
// way to handle this.
std.log.debug(
"Keep alive connection closed. Initializing a new client.",
.{},
);
self.client.deinit();
self.client = .{ .allocator = self.allocator, .io = self.io };
writer.deinit();
writer_initialized = false;
return self.fetch(request, retries - 1);
},
else => return err,
}).status;
return .{
.body = try writer.toOwnedSlice(),
.status = status,
};
}
pub fn graphql(
self: *Self,
body: []const u8,
variables: anytype,
) !Response {
const serialized = try std.json.Stringify.valueAlloc(self.allocator, .{
.query = body,
.variables = variables,
}, .{});
defer self.allocator.free(serialized);
return try self.fetch(.{
.url = "https://api.github.com/graphql",
.body = serialized,
.headers = .{
.authorization = .{ .override = self.bearer },
.content_type = .{ .override = "application/json" },
},
}, 8);
}
pub fn rest(
self: *Self,
url: []const u8,
) !Response {
return try self.fetch(.{
.url = url,
.headers = .{
.authorization = .{ .override = self.bearer },
.content_type = .{ .override = "application/json" },
},
.extra_headers = &.{
.{ .name = "X-GitHub-Api-Version", .value = "2026-03-10" },
},
}, 8);
}