// std.http S7a (PLAN-HTTPZ): a live single-worker server and its // clients driven in ONE thread via Server.tick — keep-alive reuse, // POST body echo, the per-connection request cap closing politely, // 404 routing, and half-a-header eviction at the request deadline // while the server keeps serving others. #import "modules/std.sx"; PORT :: 18933; handler :: (req: *http.Request, resp: *http.Response) { if req.path == "/hello" { resp.body = concat("hello ", req.method); return; } if req.path == "/echo" { resp.body = req.body; return; } resp.status = 404; resp.body = "nope"; } contains :: (hay: string, needle: string) -> bool { if needle.len > hay.len { return false; } i := 0; while i + needle.len <= hay.len { j := 0; ok := true; while j < needle.len { if hay[i + j] != needle[j] { ok = false; break; } j += 1; } if ok { return true; } i += 1; } return false; } // Connect a nonblocking loopback client. dial :: () -> i32 { fd := socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0); if fd < 0 { return -1; } addr : socket.SockAddr = .{ sin_len = 16, sin_family = xx socket.AF_INET, sin_port = socket.htons(PORT), sin_addr = 0x0100007F, }; if socket.connect(fd, @addr, 16) != 0 { socket.close(fd); return -1; } if !socket.set_nonblocking(fd) { socket.close(fd); return -1; } return fd; } // True when `buf[0..len]` holds a complete response (headers + body). resp_complete :: (buf: [*]u8, len: i64) -> bool { s := string.{ ptr = buf, len = xx len }; he := -1; i := 0; while i + 3 < s.len { if s[i] == 13 and s[i+1] == 10 and s[i+2] == 13 and s[i+3] == 10 { he = i; break; } i += 1; } if he < 0 { return false; } // Content-Length digits cl : i64 = 0; seen := false; j := 0; needle := "Content-Length: "; while j + needle.len < s.len { k := 0; ok := true; while k < needle.len { if s[j + k] != needle[k] { ok = false; break; } k += 1; } if ok { d := j + needle.len; while d < s.len and s[d] >= 48 and s[d] <= 57 { cl = cl * 10 + (s[d] - 48); d += 1; } seen = true; break; } j += 1; } if !seen { return false; } return len >= he + 4 + cl; } // Send a request and tick the server until its full response arrives. // Returns the response text ("" = the connection closed instead). roundtrip :: (srv: *http.Server, fd: i32, reqtext: string, scratch: [*]u8) -> string { socket.write(fd, reqtext.ptr, xx reqtext.len); total : i64 = 0; tries := 0; while tries < 400 { srv.tick(5) catch {}; n, re := socket.read_nb(fd, @scratch[total], xx (4096 - total)); if !re { total += n; } else if re == error.Closed { return string.{ ptr = scratch, len = xx total }; } if resp_complete(scratch, total) { return string.{ ptr = scratch, len = xx total }; } tries += 1; } return ""; } main :: () -> i32 { cfg : http.Config = .{ port = PORT, timeout_request_ms = 150, timeout_keepalive_ms = 400, request_count = 3, max_conn = 8, }; srv, se := http.Server.init(cfg, handler); if se { print("server init failed\n"); return 1; } buf : [4096]u8 = ---; // ── 1. GET, keep-alive default ──────────────────────────────────── c1 := dial(); if c1 < 0 { print("dial failed\n"); return 1; } r1 := roundtrip(@srv, c1, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n", @buf[0]); if !contains(r1, "HTTP/1.1 200 OK") { print("case1: bad status\n"); return 1; } if !contains(r1, "hello GET") { print("case1: bad body\n"); return 1; } if !contains(r1, "Connection: keep-alive") { print("case1: expected keep-alive\n"); return 1; } print("GET 200, keep-alive\n"); // ── 2. same socket again: the connection was actually reused ───── r2 := roundtrip(@srv, c1, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n", @buf[0]); if !contains(r2, "hello GET") { print("case2: keep-alive reuse failed\n"); return 1; } print("keep-alive reuse ok\n"); // ── 3. third request hits request_count: Connection: close + EOF ─ r3 := roundtrip(@srv, c1, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n", @buf[0]); if !contains(r3, "Connection: close") { print("case3: expected close at cap\n"); return 1; } drained := false; tries := 0; while !drained and tries < 200 { srv.tick(5) catch {}; zq, ze := socket.read_nb(c1, @buf[0], 64); if ze == error.Closed { drained = true; } if !ze and zq == 0 { drained = true; } tries += 1; } if !drained { print("case3: server did not close at the cap\n"); return 1; } socket.close(c1); print("request cap: close + EOF\n"); // ── 4. POST body echo ───────────────────────────────────────────── c2 := dial(); if c2 < 0 { print("dial2 failed\n"); return 1; } r4 := roundtrip(@srv, c2, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 9\r\n\r\nping-pong", @buf[0]); if !contains(r4, "ping-pong") { print("case4: body not echoed\n"); return 1; } print("POST echo ok\n"); // ── 5. unknown path routes 404 ──────────────────────────────────── r5 := roundtrip(@srv, c2, "GET /missing HTTP/1.1\r\nHost: t\r\n\r\n", @buf[0]); if !contains(r5, "HTTP/1.1 404 Not Found") { print("case5: expected 404\n"); return 1; } socket.close(c2); print("404 routing ok\n"); // ── 6. half a header is evicted at the request deadline, while a // healthy client keeps being served ────────────────────────── c3 := dial(); if c3 < 0 { print("dial3 failed\n"); return 1; } half := "GET /hel"; socket.write(c3, half.ptr, xx half.len); gone := event.deadline_in(300); // > timeout_request_ms while !event.expired(gone) { srv.tick(5) catch {}; } c4 := dial(); if c4 < 0 { print("dial4 failed\n"); return 1; } r6 := roundtrip(@srv, c4, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n", @buf[0]); if !contains(r6, "hello GET") { print("case6: healthy client starved\n"); return 1; } evicted := false; tries = 0; while !evicted and tries < 200 { srv.tick(5) catch {}; zq2, ze2 := socket.read_nb(c3, @buf[0], 64); if ze2 == error.Closed { evicted = true; } if !ze2 and zq2 == 0 { evicted = true; } tries += 1; } if !evicted { print("case6: half-header connection never evicted\n"); return 1; } socket.close(c3); socket.close(c4); print("slow client evicted, healthy client served\n"); srv.close(); print("http server ok\n"); return 0; }