P4.1-001: 2s read timeout on accepted sockets (idle preconnect wedged the loop)

A browser speculative preconnection sends no bytes; the sequential
accept loop blocked in read() on it forever while real requests sat in
the backlog — LAN clients saw a dead server while curl (connect+send in
one shot) worked. SO_RCVTIMEO frees the loop. Regression case pinned in
tests/server_http.sx (fails 000 pre-fix, 200 post-fix).
This commit is contained in:
agra
2026-06-12 01:47:30 +03:00
parent 734c00fb98
commit 886b48630b
3 changed files with 31 additions and 0 deletions

View File

@@ -290,6 +290,7 @@ run_server :: (store_dir: string, port: s64) -> !ServeErr {
while true {
client := sock.accept(fd, null, null);
if client < 0 { continue; }
http.set_read_timeout(client, 2);
gpa := GPA.init();
arena := Arena.init(xx gpa, 65536);

View File

@@ -46,6 +46,24 @@ listen_on :: (port: s64) -> (s32, !HttpError) {
return fd;
}
SO_RCVTIMEO :s32: 0x1006; // macOS
// macOS struct timeval (padded to 16 for the setsockopt copy).
Timeval :: struct {
tv_sec: s64;
tv_usec: s32 = 0;
pad: s32 = 0;
}
// Bound blocking reads on `fd` to `secs` seconds. A sequential accept
// loop needs this: browsers open speculative preconnections that never
// send bytes, and an unbounded read on one of those wedges the whole
// server while real requests sit in the backlog.
set_read_timeout :: (fd: s32, secs: s64) {
tv : Timeval = .{ tv_sec = secs };
sock.setsockopt(fd, sock.SOL_SOCKET, SO_RCVTIMEO, xx @tv, 16);
}
// Parse the request line `METHOD SP PATH SP HTTP/x.y` off the raw bytes.
// False when the bytes don't look like an HTTP request line.
parse_request :: (raw: string, req: *Request) -> bool {