feat: std.http read buffers grow on demand toward read_buf_cap

read_buf_cap is now the per-request LIMIT, not a preallocation: slots
start at 16K, double when full (one-step sizing when a Content-Length
declares the body), and keep their grown capacity for slot reuse. At
the limit the refusal distinguishes oversized headers (431) from an
oversized body (413). Unblocks A1: distd accepts multi-hundred-MB
artifact uploads — preallocating that per slot was never an option.
examples/1633 adds a body past the initial capacity echoing intact.
This commit is contained in:
agra
2026-06-12 21:29:13 +03:00
parent 3a97019aa7
commit 8641441fad
40 changed files with 13450 additions and 13109 deletions

View File

@@ -33,12 +33,18 @@ Config :: struct {
port: i64 = 8080;
backlog: i32 = 128;
max_conn: i64 = 256; // workers.max_conn
read_buf_cap: i64 = 65536; // workers.large_buffer_size
// MAXIMUM bytes one request (headers + body) may occupy. The
// per-connection buffer starts at READ_BUF_INITIAL and grows on
// demand toward this limit (a declared Content-Length sizes it in
// one step); a grown buffer is retained for slot reuse.
read_buf_cap: i64 = 1048576;
timeout_request_ms: i64 = 5000; // deliver a full request, or evicted
timeout_keepalive_ms: i64 = 5000;// idle between requests, or evicted
request_count: i64 = 100; // requests per connection, then close
}
READ_BUF_INITIAL :: 16384;
// One parsed request, viewed in place over the connection's read
// buffer — valid for the duration of the handler call only.
Request :: struct {
@@ -125,7 +131,8 @@ CONN_KEEPALIVE :u8: 3; // between requests (deadline: keepalive)
Conn :: struct {
fd: i32 = -1;
state: u8 = 0;
read_buf: [*]u8 = null; // cap = config.read_buf_cap, reused across connections
read_buf: [*]u8 = null; // grows toward config.read_buf_cap, reused across connections
read_cap: i64 = 0;
read_len: i64 = 0;
out_buf: [*]u8 = null; // per-response allocation, freed when sent
out_len: i64 = 0;
@@ -296,7 +303,10 @@ Server :: struct {
if !socket.set_nonblocking(fd) { socket.close(fd); return; }
c := @self.conns[slot];
if c.read_buf == null {
c.read_buf = xx self.own_alloc.alloc_bytes(self.cfg.read_buf_cap);
init_cap : i64 = READ_BUF_INITIAL;
if init_cap > self.cfg.read_buf_cap { init_cap = self.cfg.read_buf_cap; }
c.read_buf = xx self.own_alloc.alloc_bytes(init_cap);
c.read_cap = init_cap;
}
c.fd = fd;
c.state = CONN_READING;
@@ -312,6 +322,22 @@ Server :: struct {
// ── read → parse → dispatch ──────────────────────────────────────
// Grow the slot's read buffer to at least `target` (0 = double),
// never past cfg.read_buf_cap. False when already at the limit.
grow_read_buf :: (self: *Server, slot: i64, target: i64) -> bool {
c := @self.conns[slot];
want := if target > 0 then target else c.read_cap * 2;
if want <= c.read_cap { return true; }
if want > self.cfg.read_buf_cap { want = self.cfg.read_buf_cap; }
if want <= c.read_cap { return false; } // already at the limit
nb : [*]u8 = xx self.own_alloc.alloc_bytes(want);
if c.read_len > 0 { memcpy(nb, c.read_buf, xx c.read_len); }
self.own_alloc.dealloc_bytes(xx c.read_buf, c.read_cap);
c.read_buf = nb;
c.read_cap = want;
return true;
}
read_more :: (self: *Server, slot: i64) {
c := @self.conns[slot];
if c.state == CONN_KEEPALIVE {
@@ -319,12 +345,20 @@ Server :: struct {
c.deadline = event.deadline_in(self.cfg.timeout_request_ms);
}
while true {
cap := self.cfg.read_buf_cap - c.read_len;
if cap <= 0 {
self.respond_error_close(slot, 431);
return;
if c.read_len == c.read_cap {
if !self.grow_read_buf(slot, 0) {
// over the limit: oversized headers (431) or body (413)
hdr_done := false;
i : i64 = 0;
while i + 3 < c.read_len {
if c.read_buf[i] == 13 and c.read_buf[i+1] == 10 and c.read_buf[i+2] == 13 and c.read_buf[i+3] == 10 { hdr_done = true; break; }
i += 1;
}
self.respond_error_close(slot, if hdr_done then 413 else 431);
return;
}
}
nq, re := socket.read_nb(c.fd, @c.read_buf[c.read_len], xx cap);
nq, re := socket.read_nb(c.fd, @c.read_buf[c.read_len], xx (c.read_cap - c.read_len));
if re == error.WouldBlock { break; }
if re { // Closed or Fault
self.conn_close(slot);
@@ -392,7 +426,11 @@ Server :: struct {
}
total := xx he + 4 + clen;
if total > self.cfg.read_buf_cap { self.respond_error_close(slot, 413); return false; }
if c.read_len < total { return false; }
if c.read_len < total {
// size the buffer for the declared body in one step
self.grow_read_buf(slot, total);
return false;
}
req.body = string.{ ptr = @c.read_buf[he + 4], len = xx clen };
// keep-alive: 1.1 default on, 1.0 default off, header overrides