State of http middlewares in zig 0.16

Hello all,

Continuing on my Zig studies, i got this sample code of the standard zig http client and server libraries as they are presented in zig 0.16

// 3-networking.zig

const std = @import("std");
const http = std.http;
const Io = std.Io;
const net = Io.net;
const IpAddress = net.IpAddress;

fn httpClient(io: Io, allocator: std.mem.Allocator, addr: IpAddress) !void {
    // Wait for server
    io.sleep(Io.Duration.fromMilliseconds(100), .awake) catch {};

    var client = http.Client{
        .allocator = allocator,
        .io = io,
    };
    defer client.deinit();

    var uri_buf: [128]u8 = undefined;
    const uri_str = try std.fmt.bufPrint(&uri_buf, "http://127.0.0.1:{d}/", .{addr.getPort()});
    const uri = try std.Uri.parse(uri_str);

    var req = try client.request(.GET, uri, .{});
    defer req.deinit();

    try req.sendBodiless();

    var redirect_buffer: [1024]u8 = undefined;
    var response = try req.receiveHead(&redirect_buffer);

    std.log.info("Client received response: {d} {s}", .{@intFromEnum(response.head.status), response.head.reason});

    var body_buffer: [1024]u8 = undefined;
    var transfer_buffer: [1024]u8 = undefined;
    var body_reader = response.reader(&transfer_buffer);
    const n = try body_reader.readSliceShort(&body_buffer);

    std.log.info("Client received body: {s}", .{body_buffer[0..n]});
}

fn httpServer(io: Io, addr: IpAddress) !void {
    var srv = try addr.listen(io, .{ .reuse_address = true });
    defer srv.deinit(io);

    std.log.info("Server listening on {any}", .{addr});

    const stream = try srv.accept(io);
    defer stream.close(io);

    var in_buffer: [8192]u8 = undefined;
    var out_buffer: [1024]u8 = undefined;

    var reader_obj = stream.reader(io, &in_buffer);
    var writer_obj = stream.writer(io, &out_buffer);

    var server = http.Server.init(&reader_obj.interface, &writer_obj.interface);
    var req = try server.receiveHead();

    std.log.info("Server received request: {s} {s}", .{@tagName(req.head.method), req.head.target});

    try req.respond("Hello from Zig HTTP Server!\n", .{});
    std.log.info("Server responded and closing", .{});
}

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const addr = try IpAddress.parse("127.0.0.1", 8080);

    var server_task = io.async(httpServer, .{ io, addr });
    var client_task = io.async(httpClient, .{ io, init.gpa, addr });

    try server_task.await(io);
    try client_task.await(io);
}
 

If i understood correctly, old examples no longer work on 0.16

By the sake of learning, i have 2 questions:

1: How often one could expect extensive rewriting like this one in 0.16?

2: Is there a way to optimize this sample? any redundant, wasteful code?

Thanks in advance for any guidance!

Re 1: There’s no crystal ball that tells what might change significantly and require rewriting along Zig’s path to 1.0, especially the std lib. Any estimate is a gamble. The conservative assumption is that you will have to rewrite. This is a fact of life when choosing to use a pre 1.0 language.

4 Likes

I am writing a web-server based service and currently using this approach:


pub const Server = struct {
    host: []const u8,
    port: u16,
    addr: std.Io.net.IpAddress,
    io: std.Io,
    db: *Database,
    config: Config,

    pub fn init(io: std.Io, cfg: Config, database: *Database) !Server {
        const host: []const u8 = "0.0.0.0";
        const port: u16 = cfg.port;
        const addr = try std.Io.net.IpAddress.parseIp4(host, port);

        return .{ .host = host, .port = port, .addr = addr, .io = io, .db = database, .config = cfg };
    }

    pub fn run(self: Server) !void {
        var listening = try self.listen();
        while (true) {
            const connection = try listening.accept(self.io);
            self.handleConnection(connection) catch |err| {
                std.log.err("connection error: {any}", .{err});
            };
        }
    }

    fn handleConnection(self: Server, connection: std.Io.net.Stream) !void {
        defer connection.close(self.io);

        var read_buffer: [4096]u8 = undefined;
        var write_buffer: [4096]u8 = undefined;

        var stream_reader = std.Io.net.Stream.Reader.init(connection, self.io, &read_buffer);
        var stream_writer = std.Io.net.Stream.Writer.init(connection, self.io, &write_buffer);

        var http_server = std.http.Server.init(&stream_reader.interface, &stream_writer.interface);

        var request = try http_server.receiveHead();
        try self.handleRequest(&request);
    }

    fn handleRequest(self: Server, request: *std.http.Server.Request) !void {
        const route = matchRoute(request.head.method, request.head.target) orelse {
            try respondNotFound(request);
            return;
        };

        if (route.requiresAdmin() and !self.isAuthorized(request)) {
            try respondLoginForm(request, .unauthorized);
            return;
        }

        switch (route) {
            .event => try self.handleEvent(request),
            .login => try self.handleLogin(request),
            .index => try self.handleIndex(request),
            .app => try self.handleApp(request),
            .createDaily => try self.handleCreateDaily(request),
        }
    }
}

Does it look clearer?

1 Like

A few general points:

  1. Never swallow error.Canceled, something like io.sleep(Io.Duration.fromMilliseconds(100), .awake) catch {}; is a tell that the code is not well written, the proper way would be try io.sleep(.fromMilliseconds(100), .awake);.

  2. If you are always just using readSliceShort(), you don’t need any transfer buffer, you can pass an empty buffer to the reader(). The transfer buffer is only needed if you do many small reads or use the take*() functions.

  3. If readSliceShort() returns error.ReadFailed, you should reach into reader.err to recover the actual error from there, which could be error.Canceled and should be then propagated.

  4. The in_buffer in the server dictates how many HTTP headers can the request come with, you typically want a larger buffer there, at least 64k.

  5. The pattern of io.async() followed by io.async() is not safe in your example. You always have to mentally translate io.async(func, args) to func(args) and imagine what would happen. In your case, the HTTP server would be listening, and the client would never get to send it a request, because the server is blocking the thread. You need to use io.concurrent here.

8 Likes

Thanks for sharing!

Your sample covers even routing, i’ll study it.

1 Like
    fn handleConnection(self: Server, connection: std.Io.net.Stream) !void {
        defer connection.close(self.io);

        var read_buffer: [4096]u8 = undefined;
        var write_buffer: [4096]u8 = undefined;

        var stream_reader = std.Io.net.Stream.Reader.init(connection, self.io, &read_buffer);
        var stream_writer = std.Io.net.Stream.Writer.init(connection, self.io, &write_buffer);

        var http_server = std.http.Server.init(&stream_reader.interface, &stream_writer.interface);

        var request = try http_server.receiveHead();
        try self.handleRequest(&request);
    }
}

can you explain why you use std.http.Server here? I have a hard time understanding what that module is for or does (even receiveHead I don’t quite understand)

I mean that I don’t understand what std.http.Server is for, at least when compared with std.Io.net.Server. They seem to serve the same purpose, std.Io.net.Server being more generic it looks to me

Once upon a time I had an implementation based on std.Io.net. It can be done with it. And you’ll need to implement http-specific things like head, HTTP codes etc.

http.Server provides nicer tools, structures with simple way to get data from the request. And it provides quite a straightforward approach to handle the incoming request:

/// Initialize an HTTP server that can respond to multiple requests on the same
/// connection.
///
/// The buffer of `in` must be large enough to store the client's entire HTTP
/// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`.
///
/// The returned `Server` is ready for `receiveHead` to be called.
pub fn init(in: *Reader, out: *Writer) Server {...}