From 455ed79872f1551b8c9fe27040b8c58e31aa41ef Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 2 Jul 2025 16:48:14 +0800 Subject: [PATCH] Remove HTTP client generic Loop parameter I think we initially thought we might need different clients for different parts of the system, each with a unique loop (e.g. we thought telemetry might need some isolation). But that never happened, so it's just needless now, especially since the async connect uses the non-generic *Loop type directly. --- src/app.zig | 2 +- src/browser/xhr/xhr.zig | 3 +- src/http/client.zig | 368 +++++++++++++++++++++------------------- 3 files changed, 200 insertions(+), 173 deletions(-) diff --git a/src/app.zig b/src/app.zig index 841aaf7f..88599598 100644 --- a/src/app.zig +++ b/src/app.zig @@ -55,7 +55,7 @@ pub const App = struct { .telemetry = undefined, .app_dir_path = app_dir_path, .notification = notification, - .http_client = try http.Client.init(allocator, .{ + .http_client = try http.Client.init(allocator, loop, .{ .max_concurrent = 3, .http_proxy = config.http_proxy, .proxy_type = config.proxy_type, diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index e7b11630..82d43a5f 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -458,7 +458,6 @@ pub const XMLHttpRequest = struct { &self.url.?.uri, self, onHttpRequestReady, - self.loop, ); } @@ -494,7 +493,7 @@ pub const XMLHttpRequest = struct { } } - try request.sendAsync(self.loop, self, .{}); + try request.sendAsync(self, .{}); self.request = request; } diff --git a/src/http/client.zig b/src/http/client.zig index 1cb07852..5b8f755e 100644 --- a/src/http/client.zig +++ b/src/http/client.zig @@ -77,6 +77,7 @@ pub const ProxyAuth = union(enum) { // Thread-safe. Holds our root certificate, connection pool and state pool // Used to create Requests. pub const Client = struct { + loop: *Loop, req_id: usize, allocator: Allocator, state_pool: StatePool, @@ -97,7 +98,7 @@ pub const Client = struct { max_idle_connection: usize = 10, }; - pub fn init(allocator: Allocator, opts: Opts) !Client { + pub fn init(allocator: Allocator, loop: *Loop, opts: Opts) !Client { var root_ca: std.crypto.Certificate.Bundle = if (builtin.is_test) .{} else try tls.config.cert.fromSystem(allocator); errdefer root_ca.deinit(allocator); @@ -109,6 +110,7 @@ pub const Client = struct { return .{ .req_id = 0, + .loop = loop, .root_ca = root_ca, .allocator = allocator, .state_pool = state_pool, @@ -153,7 +155,6 @@ pub const Client = struct { uri: *const Uri, ctx: *anyopaque, callback: AsyncQueue.Callback, - loop: *Loop, opts: RequestOpts, ) !void { @@ -184,7 +185,7 @@ pub const Client = struct { .callback = callback, .node = .{ .func = AsyncQueue.check }, }; - _ = try loop.timeout(10 * std.time.ns_per_ms, &queue.node); + _ = try self.loop.timeout(10 * std.time.ns_per_ms, &queue.node); } // Either called directly from initAsync (if we have a state ready) @@ -257,9 +258,8 @@ pub const RequestFactory = struct { uri: *const Uri, ctx: *anyopaque, callback: AsyncQueue.Callback, - loop: *Loop, ) !void { - return self.client.initAsync(arena, method, uri, ctx, callback, loop, self.opts); + return self.client.initAsync(arena, method, uri, ctx, callback, self.opts); } }; @@ -690,19 +690,19 @@ pub const Request = struct { tls_verify_host: ?bool = null, }; // Makes an asynchronous request - pub fn sendAsync(self: *Request, loop: anytype, handler: anytype, opts: SendAsyncOpts) !void { + pub fn sendAsync(self: *Request, handler: anytype, opts: SendAsyncOpts) !void { if (opts.tls_verify_host) |override| { self._tls_verify_host = override; } try self.prepareInitialSend(); - return self.doSendAsync(loop, handler, true); + return self.doSendAsync(handler, true); } - pub fn redirectAsync(self: *Request, redirect: Reader.Redirect, loop: anytype, handler: anytype) !void { + pub fn redirectAsync(self: *Request, redirect: Reader.Redirect, handler: anytype) !void { try self.prepareToRedirect(redirect); - return self.doSendAsync(loop, handler, true); + return self.doSendAsync(handler, true); } - fn doSendAsync(self: *Request, loop: anytype, handler: anytype, use_pool: bool) !void { + fn doSendAsync(self: *Request, handler: anytype, use_pool: bool) !void { if (use_pool) { if (self.findExistingConnection(false)) |connection| { self._connection = connection; @@ -729,7 +729,9 @@ pub const Request = struct { const connection = self._connection.?; errdefer self.destroyConnection(connection); - const AsyncHandlerT = AsyncHandler(@TypeOf(handler), @TypeOf(loop)); + const loop = self._client.loop; + + const AsyncHandlerT = AsyncHandler(@TypeOf(handler)); const async_handler = try self.arena.create(AsyncHandlerT); const state = self._state; @@ -998,9 +1000,9 @@ pub const Request = struct { }; // Handles asynchronous requests -fn AsyncHandler(comptime H: type, comptime L: type) type { +fn AsyncHandler(comptime H: type) type { return struct { - loop: L, + loop: *Loop, handler: H, request: *Request, read_buf: []u8, @@ -1279,7 +1281,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { return; }; - self.request.redirectAsync(redirect, self.loop, self.handler) catch |err| { + self.request.redirectAsync(redirect, self.handler) catch |err| { self.handleError("Setup async redirect", err); return; }; @@ -1319,7 +1321,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { std.debug.assert(request._keepalive == false); request.releaseConnection(); - request.doSendAsync(self.loop, self.handler, false) catch |conn_err| { + request.doSendAsync(self.handler, false) catch |conn_err| { // You probably think it's weird that we fallthrough to the: // return true; // The caller will take the `true` and just exit. This is what @@ -3054,30 +3056,30 @@ test "HttpClient Reader: fuzz" { } test "HttpClient: invalid url" { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); const uri = try Uri.parse("http:///"); - try testing.expectError(error.UriMissingHost, client.request(.GET, &uri)); + try testing.expectError(error.UriMissingHost, tc.client.request(.GET, &uri)); } test "HttpClient: sync connect error" { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); const uri = try Uri.parse("HTTP://127.0.0.1:9920"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); try testing.expectError(error.ConnectionRefused, req.sendSync(.{})); } test "HttpClient: sync no body" { - for (0..2) |i| { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); + for (0..2) |i| { const uri = try Uri.parse("http://127.0.0.1:9582/http_client/simple"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{}); @@ -3094,12 +3096,12 @@ test "HttpClient: sync no body" { } test "HttpClient: sync tls no body" { - for (0..1) |_| { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); + for (0..2) |_| { const uri = try Uri.parse("https://127.0.0.1:9581/http_client/simple"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{ .tls_verify_host = false }); @@ -3113,12 +3115,12 @@ test "HttpClient: sync tls no body" { } test "HttpClient: sync with body" { - for (0..2) |i| { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); + for (0..2) |i| { const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{}); @@ -3138,13 +3140,13 @@ test "HttpClient: sync with body" { } test "HttpClient: sync with body proxy CONNECT" { - for (0..2) |i| { - const proxy_uri = try Uri.parse("http://127.0.0.1:9582/"); - var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri }); - defer client.deinit(); + const proxy_uri = try Uri.parse("http://127.0.0.1:9582/"); + var tc = try TestContext.init(.{ .proxy_type = .connect, .http_proxy = proxy_uri }); + defer tc.deinit(); + for (0..2) |i| { const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{}); @@ -3167,11 +3169,11 @@ test "HttpClient: sync with body proxy CONNECT" { test "HttpClient: basic authentication CONNECT" { const proxy_uri = try Uri.parse("http://127.0.0.1:9582/"); - var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .basic = .{ .user_pass = "user:pass" } } }); - defer client.deinit(); + var tc = try TestContext.init(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .basic = .{ .user_pass = "user:pass" } } }); + defer tc.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{}); @@ -3184,13 +3186,14 @@ test "HttpClient: basic authentication CONNECT" { try testing.expectEqual(null, res.header.get("__authorization")); try testing.expectEqual("Basic dXNlcjpwYXNz", res.header.get("__proxy-authorization")); } + test "HttpClient: bearer authentication CONNECT" { const proxy_uri = try Uri.parse("http://127.0.0.1:9582/"); - var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .bearer = .{ .token = "fruitsalad" } } }); - defer client.deinit(); + var tc = try TestContext.init(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .bearer = .{ .token = "fruitsalad" } } }); + defer tc.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{}); @@ -3205,12 +3208,12 @@ test "HttpClient: bearer authentication CONNECT" { } test "HttpClient: sync with gzip body" { - for (0..2) |i| { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); + for (0..2) |i| { const uri = try Uri.parse("http://127.0.0.1:9582/http_client/gzip"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{}); @@ -3224,17 +3227,18 @@ test "HttpClient: sync with gzip body" { } test "HttpClient: sync tls with body" { + var tc = try TestContext.init(.{}); + defer tc.deinit(); + var arr: std.ArrayListUnmanaged(u8) = .{}; defer arr.deinit(testing.allocator); try arr.ensureTotalCapacity(testing.allocator, 20); - var client = try testClient(.{}); - defer client.deinit(); for (0..5) |_| { defer arr.clearRetainingCapacity(); const uri = try Uri.parse("https://127.0.0.1:9581/http_client/body"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{ .tls_verify_host = false }); @@ -3252,17 +3256,18 @@ test "HttpClient: sync tls with body" { } test "HttpClient: sync redirect from TLS to Plaintext" { + var tc = try TestContext.init(.{}); + defer tc.deinit(); + var arr: std.ArrayListUnmanaged(u8) = .{}; defer arr.deinit(testing.allocator); try arr.ensureTotalCapacity(testing.allocator, 20); for (0..5) |_| { defer arr.clearRetainingCapacity(); - var client = try testClient(.{}); - defer client.deinit(); const uri = try Uri.parse("https://127.0.0.1:9581/http_client/redirect/insecure"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{ .tls_verify_host = false }); @@ -3282,17 +3287,18 @@ test "HttpClient: sync redirect from TLS to Plaintext" { } test "HttpClient: sync redirect plaintext to TLS" { + var tc = try TestContext.init(.{}); + defer tc.deinit(); + var arr: std.ArrayListUnmanaged(u8) = .{}; defer arr.deinit(testing.allocator); try arr.ensureTotalCapacity(testing.allocator, 20); for (0..5) |_| { defer arr.clearRetainingCapacity(); - var client = try testClient(.{}); - defer client.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/redirect/secure"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{ .tls_verify_host = false }); @@ -3309,11 +3315,11 @@ test "HttpClient: sync redirect plaintext to TLS" { } test "HttpClient: sync GET redirect" { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/redirect"); - var req = try client.request(.GET, &uri); + var req = try tc.client.request(.GET, &uri); defer req.deinit(); var res = try req.sendSync(.{ .tls_verify_host = false }); @@ -3329,8 +3335,9 @@ test "HttpClient: sync GET redirect" { test "HttpClient: async connect error" { defer testing.reset(); - var loop = try Loop.init(testing.allocator); - defer loop.deinit(); + + var tc = try TestContext.init(.{}); + defer tc.deinit(); const Handler = struct { loop: *Loop, @@ -3338,7 +3345,7 @@ test "HttpClient: async connect error" { fn requestReady(ctx: *anyopaque, req: *Request) !void { const self: *@This() = @alignCast(@ptrCast(ctx)); - try req.sendAsync(self.loop, self, .{}); + try req.sendAsync(self, .{}); } fn onHttpResponse(self: *@This(), res: anyerror!Progress) !void { @@ -3355,27 +3362,24 @@ test "HttpClient: async connect error" { }; var reset: Thread.ResetEvent = .{}; - var client = try testClient(.{}); - defer client.deinit(); var handler = Handler{ - .loop = &loop, + .loop = tc.loop, .reset = &reset, }; const uri = try Uri.parse("HTTP://127.0.0.1:9920"); - try client.initAsync( + try tc.client.initAsync( testing.arena_allocator, .GET, &uri, &handler, Handler.requestReady, - &loop, .{}, ); for (0..10) |_| { - try loop.io.run_for_ns(std.time.ns_per_ms * 10); + try tc.loop.io.run_for_ns(std.time.ns_per_ms * 10); if (reset.isSet()) { break; } @@ -3387,14 +3391,14 @@ test "HttpClient: async connect error" { test "HttpClient: async no body" { defer testing.reset(); - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); - var handler = try CaptureHandler.init(); + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("HTTP://127.0.0.1:9582/http_client/simple"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); try handler.waitUntilDone(); const res = handler.response; @@ -3406,14 +3410,14 @@ test "HttpClient: async no body" { test "HttpClient: async with body" { defer testing.reset(); - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); - var handler = try CaptureHandler.init(); + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("HTTP://127.0.0.1:9582/http_client/echo"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); try handler.waitUntilDone(); const res = handler.response; @@ -3431,14 +3435,14 @@ test "HttpClient: async with body" { test "HttpClient: async with gzip body" { defer testing.reset(); - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); - var handler = try CaptureHandler.init(); + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("HTTP://127.0.0.1:9582/http_client/gzip"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); try handler.waitUntilDone(); const res = handler.response; @@ -3454,14 +3458,14 @@ test "HttpClient: async with gzip body" { test "HttpClient: async redirect" { defer testing.reset(); - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); - var handler = try CaptureHandler.init(); + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("HTTP://127.0.0.1:9582/http_client/redirect"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); // Called twice on purpose. The initial GET resutls in the # of pending // events to reach 0. This causes our `run_for_ns` to return. But we then @@ -3484,14 +3488,15 @@ test "HttpClient: async redirect" { test "HttpClient: async tls no body" { defer testing.reset(); - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); + for (0..5) |_| { - var handler = try CaptureHandler.init(); + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("HTTPs://127.0.0.1:9581/http_client/simple"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); try handler.waitUntilDone(); const res = handler.response; @@ -3508,15 +3513,15 @@ test "HttpClient: async tls no body" { test "HttpClient: async tls with body" { defer testing.reset(); - for (0..5) |_| { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); - var handler = try CaptureHandler.init(); + for (0..5) |_| { + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("HTTPs://127.0.0.1:9581/http_client/body"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); try handler.waitUntilDone(); const res = handler.response; @@ -3532,15 +3537,15 @@ test "HttpClient: async tls with body" { test "HttpClient: async redirect from TLS to Plaintext" { defer testing.reset(); - for (0..1) |_| { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); - var handler = try CaptureHandler.init(); + for (0..2) |_| { + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("https://127.0.0.1:9581/http_client/redirect/insecure"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); try handler.waitUntilDone(); const res = handler.response; @@ -3558,15 +3563,15 @@ test "HttpClient: async redirect from TLS to Plaintext" { test "HttpClient: async redirect plaintext to TLS" { defer testing.reset(); - for (0..5) |_| { - var client = try testClient(.{}); - defer client.deinit(); + var tc = try TestContext.init(.{}); + defer tc.deinit(); - var handler = try CaptureHandler.init(); + for (0..5) |_| { + var handler = tc.captureHandler(); defer handler.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/redirect/secure"); - try client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, &handler.loop, .{}); + try tc.client.initAsync(testing.arena_allocator, .GET, &uri, &handler, CaptureHandler.requestReady, .{}); try handler.waitUntilDone(); const res = handler.response; @@ -3670,67 +3675,6 @@ const TestResponse = struct { } }; -const CaptureHandler = struct { - loop: Loop, - reset: Thread.ResetEvent, - response: TestResponse, - - fn init() !CaptureHandler { - return .{ - .reset = .{}, - .response = TestResponse.init(), - .loop = try Loop.init(testing.allocator), - }; - } - - fn deinit(self: *CaptureHandler) void { - self.response.deinit(); - self.loop.deinit(); - } - - fn requestReady(ctx: *anyopaque, req: *Request) !void { - const self: *CaptureHandler = @alignCast(@ptrCast(ctx)); - try req.sendAsync(&self.loop, self, .{ .tls_verify_host = false }); - } - - fn onHttpResponse(self: *CaptureHandler, progress_: anyerror!Progress) !void { - self.process(progress_) catch |err| { - std.debug.print("capture handler error: {}\n", .{err}); - }; - } - - fn process(self: *CaptureHandler, progress_: anyerror!Progress) !void { - const progress = try progress_; - const allocator = self.response.arena.allocator(); - try self.response.body.appendSlice(allocator, progress.data orelse ""); - if (progress.first) { - std.debug.assert(!progress.done); - self.response.status = progress.header.status; - try self.response.headers.ensureTotalCapacity(allocator, progress.header.headers.items.len); - for (progress.header.headers.items) |header| { - self.response.headers.appendAssumeCapacity(.{ - .name = try allocator.dupe(u8, header.name), - .value = try allocator.dupe(u8, header.value), - }); - } - } - - if (progress.done) { - self.reset.set(); - } - } - - fn waitUntilDone(self: *CaptureHandler) !void { - for (0..20) |_| { - try self.loop.io.run_for_ns(std.time.ns_per_ms * 25); - if (self.reset.isSet()) { - return; - } - } - return error.TimeoutWaitingForRequestToComplete; - } -}; - fn testReader(state: *State, res: *TestResponse, data: []const u8) !void { var status: u16 = 0; var r = Reader.init(state); @@ -3772,8 +3716,92 @@ fn testReader(state: *State, res: *TestResponse, data: []const u8) !void { return error.NeverDone; } -fn testClient(opts: Client.Opts) !Client { - var o = opts; - o.max_concurrent = 1; - return try Client.init(testing.allocator, o); -} +const TestContext = struct { + loop: *Loop, + client: Client, + + fn init(opts: Client.Opts) !TestContext { + const loop = try testing.allocator.create(Loop); + errdefer testing.allocator.destroy(loop); + + loop.* = try Loop.init(testing.allocator); + errdefer loop.deinit(); + + var o = opts; + o.max_concurrent = 1; + + const client = try Client.init(testing.allocator, loop, o); + errdefer client.deinit(); + + return .{ + .loop = loop, + .client = client, + }; + } + + fn deinit(self: *TestContext) void { + self.loop.deinit(); + self.client.deinit(); + testing.allocator.destroy(self.loop); + } + + fn captureHandler(self: *TestContext) CaptureHandler { + return .{ + .reset = .{}, + .loop = self.loop, + .response = TestResponse.init(), + }; + } +}; + +const CaptureHandler = struct { + loop: *Loop, + reset: Thread.ResetEvent, + response: TestResponse, + + fn deinit(self: *CaptureHandler) void { + self.response.deinit(); + } + + fn requestReady(ctx: *anyopaque, req: *Request) !void { + const self: *CaptureHandler = @alignCast(@ptrCast(ctx)); + try req.sendAsync(self, .{ .tls_verify_host = false }); + } + + fn onHttpResponse(self: *CaptureHandler, progress_: anyerror!Progress) !void { + self.process(progress_) catch |err| { + std.debug.print("capture handler error: {}\n", .{err}); + }; + } + + fn process(self: *CaptureHandler, progress_: anyerror!Progress) !void { + const progress = try progress_; + const allocator = self.response.arena.allocator(); + try self.response.body.appendSlice(allocator, progress.data orelse ""); + if (progress.first) { + std.debug.assert(!progress.done); + self.response.status = progress.header.status; + try self.response.headers.ensureTotalCapacity(allocator, progress.header.headers.items.len); + for (progress.header.headers.items) |header| { + self.response.headers.appendAssumeCapacity(.{ + .name = try allocator.dupe(u8, header.name), + .value = try allocator.dupe(u8, header.value), + }); + } + } + + if (progress.done) { + self.reset.set(); + } + } + + fn waitUntilDone(self: *CaptureHandler) !void { + for (0..20) |_| { + try self.loop.io.run_for_ns(std.time.ns_per_ms * 25); + if (self.reset.isSet()) { + return; + } + } + return error.TimeoutWaitingForRequestToComplete; + } +};