Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,11 @@ fn readHttpRequest(allocator: std.mem.Allocator, stream: *std.Io.net.Stream, max
var chunk: [request_read_chunk]u8 = undefined;

while (true) {
const n = try reader.interface.readSliceShort(&chunk);
var data: [1][]u8 = .{&chunk};
const n = reader.interface.readVec(&data) catch |err| switch (err) {
error.EndOfStream => return null,
else => |e| return e,
};
if (n == 0) return null;

try buffer.appendSlice(allocator, chunk[0..n]);
Expand Down Expand Up @@ -633,6 +637,89 @@ test "worker_protocol hasExplicitPath identifies explicit path URLs" {
try std.testing.expect(worker_protocol.hasExplicitPath("http://localhost:3000/webhook"));
}

// Integration test: covers the accept-read-write loop in main.zig that previously
// regressed when readSliceShort replaced one-shot semantics. The 340 unit tests
// invoke api.handleRequest directly and would not catch a stall in the live HTTP
// path.
//
// We bind a real loopback socket on a kernel-assigned port, connect a client
// from the same thread, send a small HTTP request without ever closing the
// write side, and call readHttpRequest on the accepted Stream. Pre-fix, this
// would block until the client FIN'd or the kernel buffer filled — neither
// happens here, so the test would hang. To make a regression fail loudly
// instead of hanging, we set SO_RCVTIMEO on the server-side socket so the
// reader returns an error within a small bound.
test "readHttpRequest does not block when request is smaller than chunk" {
if (builtin.os.tag == .windows) return error.SkipZigTest;

const c_sys = std.posix.system;

// 1. Listen on 127.0.0.1:0
const server_addr: std.Io.net.IpAddress = .{ .ip4 = std.Io.net.Ip4Address.loopback(0) };
var server = try server_addr.listen(std_compat.io(), .{ .reuse_address = true });
defer server.deinit(std_compat.io());

const port = server.socket.address.ip4.port;

// 2. Plain libc client socket — single-threaded: kernel completes 3-way
// handshake on connect against the listening socket, so accept on the
// same thread returns immediately afterwards.
const sock_rc = c_sys.socket(std.posix.AF.INET, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP);
try std.testing.expect(sock_rc >= 0);
const client_fd: std.posix.fd_t = @intCast(sock_rc);
defer _ = c_sys.close(client_fd);

var sin: std.posix.sockaddr.in = .{
.family = std.posix.AF.INET,
.port = std.mem.nativeToBig(u16, port),
.addr = std.mem.nativeToBig(u32, 0x7F000001),
};
const cr = c_sys.connect(client_fd, @ptrCast(&sin), @sizeOf(@TypeOf(sin)));
try std.testing.expectEqual(@as(c_int, 0), cr);

// 3. accept on the server side. On the bug version, the read below would
// block; the connection itself accepts fine.
var conn = try server.accept(std_compat.io());
defer conn.close(std_compat.io());

// 4. Set a 2 second receive timeout on the server-side socket. With the
// fix, the read returns in <1 ms. With the regression, it would block
// indefinitely; the timeout turns that into a clean failure rather
// than a hung test.
const tv: std.posix.timeval = .{ .sec = 2, .usec = 0 };
_ = c_sys.setsockopt(
conn.socket.handle,
std.posix.SOL.SOCKET,
std.posix.SO.RCVTIMEO,
@ptrCast(&tv),
@sizeOf(@TypeOf(tv)),
);

// 5. Send the request from the client side. Crucially we do NOT shut down
// the write side — there is no FIN, so a read that waits for EOF would
// block forever (modulo the timeout above).
const req = "GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n";
const sent = c_sys.send(client_fd, req.ptr, req.len, std.posix.MSG.NOSIGNAL);
try std.testing.expectEqual(@as(isize, @intCast(req.len)), sent);

// 6. Time the read.
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();

const t0 = ids.nowMs();
const parsed = try readHttpRequest(arena.allocator(), &conn, max_request_size);
const elapsed_ms = ids.nowMs() - t0;

try std.testing.expect(parsed != null);
try std.testing.expectEqualStrings("GET", parsed.?.method);
try std.testing.expectEqualStrings("/health", parsed.?.target);

// The fix delivers single-syscall recv semantics. 200 ms is comfortably
// above any plausible loopback variance and far below the 2 s timeout
// and the ~5 s the regression would take to surface.
try std.testing.expect(elapsed_ms < 200);
}

comptime {
_ = @import("ids.zig");
_ = @import("types.zig");
Expand Down