Panic `BADF` on a socket

I have some code that works fine for tests using the std.io.<Reader/Writer>.fixed buffer, but when I try to use sockets it fails. I think I’m just not confident enough in socket programming in Zig, combined with the new IO interface.

const std = @import("std");
const testing = std.testing;

const src = @embedFile("index.html");
const responseBufferSize = src.len + 64;

fn respondToRequest(req: *std.http.Server.Request) !void {
    if (req.head.method != .GET) {
        try req.respond("", .{ .status = .method_not_allowed, .reason = "Method Not Allowed" });
        return;
    }

    if (!std.ascii.eqlIgnoreCase(req.head.target, "/") and !std.ascii.eqlIgnoreCase(req.head.target, "/index.html")) {
        try req.respond("", .{ .status = .not_found, .reason = "Not Found" });
        return;
    }

    try req.respond(src, .{});
}

pub fn main() !void {
    const address = try std.net.Address.parseIp4("127.0.0.1", 8080);

    var sock = try address.listen(std.net.Address.ListenOptions{ .reuse_address = true });
    defer sock.deinit();

    std.debug.print("Listening on 127.0.0.1:{d}!\n", .{address.getPort()});

    while (true) {
        std.debug.print("Waiting for a connection...\n", .{});
        var conn = try sock.accept();
        defer conn.stream.close();

        var input: [1024]u8 = undefined;
        var output: [1024]u8 = undefined;
        @memset(&input, 0);
        @memset(&output, 0);

        var writer = conn.stream.writer(&output).interface;
        var reader = conn.stream.reader(&input);
        var server = std.http.Server.init(reader.interface(), &writer);

        var req = try server.receiveHead();
        try respondToRequest(&req);
    }
}

test "404" {
    const input = "GET /this/path/doesnt/exist HTTP/1.0\r\nSomeHeaderKey: SomeHeaderValue\r\n\r\n";
    var output: [1024]u8 = undefined;
    @memset(&output, 0);

    var reader = std.io.Reader.fixed(input);
    var writer = std.io.Writer.fixed(&output);
    var server = std.http.Server.init(&reader, &writer);

    var req = try server.receiveHead();
    try respondToRequest(&req);
    try testing.expectStringStartsWith(&output, "HTTP/1.1 404 Not Found\r\n");
}

test "405" {
    const input = "POST /index.html HTTP/1.0\r\nSomeHeaderKey: SomeHeaderValue\r\n\r\n";
    var output: [1024]u8 = undefined;
    @memset(&output, 0);

    var reader = std.io.Reader.fixed(input);
    var writer = std.io.Writer.fixed(&output);
    var server = std.http.Server.init(&reader, &writer);

    var req = try server.receiveHead();
    try respondToRequest(&req);
    try testing.expectStringStartsWith(&output, "HTTP/1.1 405 Method Not Allowed\r\n");
}

test "200" {
    const input = "GET /index.html HTTP/1.0\r\nSomeHeaderKey: SomeHeaderValue\r\n\r\n";
    var output: [1024]u8 = undefined;
    @memset(&output, 0);

    var reader = std.io.Reader.fixed(input);
    var writer = std.io.Writer.fixed(&output);
    var server = std.http.Server.init(&reader, &writer);

    var req = try server.receiveHead();
    try respondToRequest(&req);
    try testing.expectStringStartsWith(&output, "HTTP/1.1 200 OK\r\n");
    try testing.expect(std.mem.indexOf(u8, &output, src) != null);
}

I tried this code with curl http://localhost:8080/, but get this error:

Listening on 127.0.0.1:8080!
Waiting for a connection...
thread 791807 panic: reached unreachable code
/opt/homebrew/Cellar/zig/0.15.1/lib/zig/std/posix.zig:6176:26: 0x102f65e97 in sendmsg (webServer)
                .BADF => unreachable, // always a race condition
                         ^
/opt/homebrew/Cellar/zig/0.15.1/lib/zig/std/net.zig:2297:50: 0x102f5f30f in drain (webServer)
                return io_w.consume(posix.sendmsg(w.file_writer.file.handle, &msg, flags) catch |err| {
                                                 ^
/opt/homebrew/Cellar/zig/0.15.1/lib/zig/std/Io/Writer.zig:316:39: 0x102eda687 in defaultFlush (webServer)
    while (w.end != 0) _ = try drainFn(w, &.{""}, 1);
                                      ^
/opt/homebrew/Cellar/zig/0.15.1/lib/zig/std/Io/Writer.zig:310:26: 0x102efa02b in flush (webServer)
    return w.vtable.flush(w);
                         ^
/opt/homebrew/Cellar/zig/0.15.1/lib/zig/std/http/Server.zig:329:37: 0x102f595fb in respond (webServer)
        try request.server.out.flush();
                                    ^
/Users/tmp/Projects/webServer/src/main.zig:18:20: 0x102f594b3 in respondToRequest (webServer)
    try req.respond(src, .{});
                   ^
/Users/tmp/Projects/webServer/src/main.zig:44:29: 0x102f59ad7 in main (webServer)
        try respondToRequest(&req);
                            ^
/opt/homebrew/Cellar/zig/0.15.1/lib/zig/std/start.zig:627:37: 0x102f5abab in main (webServer)
            const result = root.main() catch |err| {
                                    ^
???:?:?: 0x1809b1d53 in ??? (???)
???:?:?: 0x0 in ??? (???)

I think you are running into the interface copying problem. See Zig 0.15.1 reader/writer: Don't make copies of @fieldParentPtr()-based interfaces for more conversation.

The short answer is to not copy the reader and writer from the stream.

var writer = conn.stream.writer(&output);
var reader = conn.stream.reader(&input);
var server = std.http.Server.init(reader.interface(), &writer.interface); 

To breakdown your issue, you wer copying the writer interface and then passing a pointer to that. This lost the parent fields required because it is now in a local variable that does not have the appopriate fields. The solution is to not capture the interface, but instead capture the parent field and pass a direct reference to the init function

That is a frustrating change… Thank you for the fix and the link with more explanation1