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

@@ -155,6 +155,34 @@ main :: () -> i32 {
socket.close(c2);
print("404 routing ok\n");
// ── 5b. a body past READ_BUF_INITIAL forces the buffer to grow ────
big_n := 50000;
payload : [*]u8 = xx context.allocator.alloc_bytes(xx big_n);
f := 0;
while f < big_n { payload[f] = 97 + cast(u8)(f % 26); f += 1; }
breq := concat("POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: ", concat(int_to_string(big_n), "\r\n\r\n"));
breq = concat(breq, string.{ ptr = payload, len = xx big_n });
c2b := dial();
if c2b < 0 { print("dial2b failed\n"); return 1; }
bigbuf : [*]u8 = xx context.allocator.alloc_bytes(xx (big_n + 4096));
socket.write(c2b, breq.ptr, xx breq.len);
btotal : i64 = 0;
btries := 0;
while btries < 2000 {
srv.tick(5) catch {};
bn, bre := socket.read_nb(c2b, @bigbuf[btotal], xx (big_n + 4096 - btotal));
if !bre { btotal += bn; }
else if bre == error.Closed { break; }
if resp_complete(bigbuf, btotal) { break; }
btries += 1;
}
bresp := string.{ ptr = bigbuf, len = xx btotal };
if !contains(bresp, "HTTP/1.1 200 OK") { print("case5b: big echo not 200\n"); return 1; }
if !contains(bresp, concat("Content-Length: ", int_to_string(big_n))) { print("case5b: wrong echo length\n"); return 1; }
if bigbuf[btotal - 1] != 97 + cast(u8)((big_n - 1) % 26) { print("case5b: tail byte wrong\n"); return 1; }
socket.close(c2b);
print("big body grows the buffer and echoes intact\n");
// ── 6. half a header is evicted at the request deadline, while a
// healthy client keeps being served ──────────────────────────
c3 := dial();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -917,6 +917,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1409,6 +1409,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1425,6 +1425,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

File diff suppressed because it is too large Load Diff

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

File diff suppressed because it is too large Load Diff

View File

@@ -1427,6 +1427,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1403,6 +1403,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -1401,6 +1401,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -924,6 +924,9 @@ declare void @Server.run(ptr, ptr) #0
; Function Attrs: nounwind
declare void @Server.accept_ready(ptr, ptr) #0
; Function Attrs: nounwind
declare i1 @Server.grow_read_buf(ptr, ptr, i64, i64) #0
; Function Attrs: nounwind
declare void @Server.read_more(ptr, ptr, i64) #0

View File

@@ -3,5 +3,6 @@ keep-alive reuse ok
request cap: close + EOF
POST echo ok
404 routing ok
big body grows the buffer and echoes intact
slow client evicted, healthy client served
http server ok

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