From 2a8e51c2d2a6334c70b5ab01299747e7e8aa9216 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sun, 31 Aug 2025 20:14:55 +0800 Subject: [PATCH 1/3] Pre-size the destination buffer when we know the response content length --- src/browser/ScriptManager.zig | 9 ++++++++- src/browser/xhr/xhr.zig | 7 +++++++ src/http/Client.zig | 10 ++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index facd86a6..e13ccbbf 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -480,7 +480,14 @@ const PendingScript = struct { // will fail. This assertion exists to catch incorrect assumptions about // how libcurl works, or about how we've configured it. std.debug.assert(self.script.source.remote.capacity == 0); - self.script.source = .{ .remote = self.manager.buffer_pool.get() }; + var buffer = self.manager.buffer_pool.get(); + if (transfer.getContentLength()) |cl| { + if (cl > 100 * 1024 * 1024) { + return error.ResponseTooLarge; + } + try buffer.ensureTotalCapacity(self.manager.allocator, cl); + } + self.script.source = .{ .remote = buffer }; } fn dataCallback(self: *PendingScript, transfer: *Http.Transfer, data: []const u8) !void { diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 76ca98e9..2709f00b 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -438,6 +438,13 @@ pub const XMLHttpRequest = struct { self.state = .loading; self.dispatchEvt("readystatechange"); + + if (transfer.getContentLength()) |cl| { + if (cl > 100 * 1024 * 1024) { + return error.ResponseTooLarge; + } + try self.response_bytes.ensureTotalCapacity(self.arena, cl); + } } fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void { diff --git a/src/http/Client.zig b/src/http/Client.zig index 02e45207..59625978 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -1040,6 +1040,16 @@ pub const Transfer = struct { try req.done_callback(req.ctx); } + + pub fn getContentLength(self: *const Transfer) ?u32 { + // It's possible for this to be null even with correct code, due to + // request fulfillment. If transfer.fulfill is called, we won't have + // a handle. + const handle = self._handle orelse return null; + + const cl = getResponseHeader(handle.conn.easy, "content-length", 0) orelse return null; + return std.fmt.parseInt(u32, cl.value, 10) catch null; + } }; pub const ResponseHeader = struct { From 57dc303d906c8938ddc197e0433e6061c4233997 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 1 Sep 2025 18:40:50 +0800 Subject: [PATCH 2/3] Make getContentLength work on fulfilled responses --- src/http/Client.zig | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/http/Client.zig b/src/http/Client.zig index 59625978..20ce82ee 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -1041,14 +1041,33 @@ pub const Transfer = struct { try req.done_callback(req.ctx); } + // This function should be called during the dataCallback. Calling it after + // such as in the doneCallback is guaranteed to return null. pub fn getContentLength(self: *const Transfer) ?u32 { - // It's possible for this to be null even with correct code, due to - // request fulfillment. If transfer.fulfill is called, we won't have - // a handle. - const handle = self._handle orelse return null; + const cl = self.getContentLengthRawValue() orelse return null; + return std.fmt.parseInt(u32, cl, 10) catch null; + } - const cl = getResponseHeader(handle.conn.easy, "content-length", 0) orelse return null; - return std.fmt.parseInt(u32, cl.value, 10) catch null; + fn getContentLengthRawValue(self: *const Transfer) ?[]const u8 { + if (self._handle) |handle| { + // If we have a handle, than this is a normal request. We can get the + // header value from the easy handle. + const cl = getResponseHeader(handle.conn.easy, "content-length", 0) orelse return null; + return cl.value; + } + + // If we have no handle, then maybe this is being called after the + // doneCallback. OR, maybe this is a "fulfilled" request. Let's check + // the injected headers (if we have any). + + const rh = self.response_header orelse return null; + for (rh._injected_headers) |hdr| { + if (std.ascii.eqlIgnoreCase(hdr.name, "content-length")) { + return hdr.value; + } + } + + return null; } }; From 785a8da623c79c03d8fe2513bdad87a642e70556 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 1 Sep 2025 18:53:00 +0800 Subject: [PATCH 3/3] remove content-length limit --- src/browser/ScriptManager.zig | 3 --- src/browser/xhr/xhr.zig | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index e13ccbbf..269d1de7 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -482,9 +482,6 @@ const PendingScript = struct { std.debug.assert(self.script.source.remote.capacity == 0); var buffer = self.manager.buffer_pool.get(); if (transfer.getContentLength()) |cl| { - if (cl > 100 * 1024 * 1024) { - return error.ResponseTooLarge; - } try buffer.ensureTotalCapacity(self.manager.allocator, cl); } self.script.source = .{ .remote = buffer }; diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 2709f00b..09891c20 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -440,9 +440,6 @@ pub const XMLHttpRequest = struct { self.dispatchEvt("readystatechange"); if (transfer.getContentLength()) |cl| { - if (cl > 100 * 1024 * 1024) { - return error.ResponseTooLarge; - } try self.response_bytes.ensureTotalCapacity(self.arena, cl); } }