Help with basic web app with std.Io.net.IpAddress

I am trying to create a basic web app. It is not responding in the browser.

const std = @import("std");
const routes = @import("routes.zig");
const posix = std.posix;
const c = @cImport({
    @cInclude("sys/socket.h");
});

const Allocator = std.mem.Allocator;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    std.debug.print("Starting Zyflow Zig application...\n", .{});

    // Start HTTP server
    // Note: This is a simplified example. In production, you'd use Jetzig framework
    // For now, we'll create a basic HTTP server structure
    std.debug.print("Starting HTTP server...\n", .{});
    try startServer(allocator);
}

fn startServer(allocator: Allocator) !void {
    std.debug.print("Initializing Io...\n", .{});
    // âś… Concrete Io implementation
    var threaded: std.Io.Threaded = .init(allocator);
    defer threaded.deinit();
    const io = threaded.io(); // <- this is the std.Io value you pass around

    std.debug.print("Parsing IP address...\n", .{});
    const address: std.Io.net.IpAddress = try std.Io.net.IpAddress.parseIp4("127.0.0.1", 8080);

    std.debug.print("Starting to listen on address...\n", .{});
    var server = try address.listen(io, .{
        .reuse_address = true,
    });
    defer server.deinit(io);

    std.debug.print("Server listening on http://127.0.0.1:8080\n", .{});

    while (true) {
        var stream = server.accept(io) catch |err| {
            std.debug.print("Error accepting connection: {}\n", .{err});
            continue;
        };
        defer stream.close(io);

        // Handle request in a simple way
        // In production, use Jetzig's routing system
        var buffer: [4096]u8 = undefined;
        var reader = stream.reader(io, &buffer);

        // Read HTTP request - readSliceShort reads available data
        // This should work for both nc and curl
        const bytes_read = reader.interface.readSliceShort(&buffer) catch {
            continue;
        };

        if (bytes_read == 0) {
            continue;
        }

        const request = buffer[0..bytes_read];

        const response = routes.handleRequest(allocator, request) catch |err| {
            std.debug.print("Error handling request: {}\n", .{err});
            const error_response = try std.fmt.allocPrint(
                allocator,
                "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nInternal Server Error",
                .{},
            );
            defer allocator.free(error_response);
            var write_buffer: [4096]u8 = undefined;
            var writer = stream.writer(io, &write_buffer);
            writer.interface.writeAll(error_response) catch {};
            writer.interface.flush() catch {};
            continue;
        };
        defer allocator.free(response);

        // Write response
        var write_buffer: [8192]u8 = undefined;
        var writer = stream.writer(io, &write_buffer);

        // Write response in chunks if larger than buffer
        var remaining = response;
        while (remaining.len > 0) {
            const chunk_size = @min(remaining.len, write_buffer.len);
            const chunk = remaining[0..chunk_size];

            writer.interface.writeAll(chunk) catch {
                break;
            };

            remaining = remaining[chunk_size..];
        }

        // Flush to ensure all data is sent
        writer.interface.flush() catch {};

        // Connection will be closed by defer stream.close(io)
    }
}

routes.zig



const std = @import("std");

const Allocator = std.mem.Allocator;

pub fn handleRequest(
    allocator: Allocator,
    request: []const u8,
) ![]const u8 {
    // Simple HTTP request parsing
    const request_str = try std.fmt.allocPrint(allocator, "{s}", .{request});
    defer allocator.free(request_str);

    // Extract method and path (simplified HTTP parsing)
    // Look for "GET /path" or "POST /path" patterns
    if (std.mem.startsWith(u8, request, "GET /")) {
        // Match "GET /" or "GET / HTTP" or "GET / HTTP/1.1" etc.
        return handleIndex(allocator);
    }

    // 404 Not Found
    return try createResponse(allocator, 404, "text/html", "<html><body><h1>404 Not Found</h1></body></html>");
}

/// Index route with Hotwire Turbo support
fn handleIndex(allocator: Allocator) ![]const u8 {
    const html =
        \\<!DOCTYPE html>
        \\<html>
        \\<head>
        \\    <title>Zyflow Zig</title>
        \\    <script src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8/dist/turbo.es2017-esm.js" type="module"></script>
        \\    <style>
        \\        body { font-family: Arial, sans-serif; margin: 40px; }
        \\        .nav { margin-bottom: 20px; }
        \\        .nav a { margin-right: 20px; padding: 10px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; }
        \\        .nav a:hover { background: #0056b3; }
        \\    </style>
        \\</head>
        \\<body>
        \\    <h1>Zyflow Zig - Low Code Application</h1>
        \\    <div class="nav">
        \\        <a href="/zig-query">Zig Query Route</a>
        \\        <a href="/js-query">QuickJS Query Route</a>
        \\    </div>
        \\    <div id="content">
        \\        <p>Welcome to Zyflow Zig! This is a low-code application built with:</p>
        \\        <ul>
        \\            <li>Zig programming language</li>
        \\            <li>PostgreSQL with connection pooling</li>
        \\            <li>QuickJS for JavaScript execution</li>
        \\            <li>Zmpl templates (when using Jetzig)</li>
        \\            <li>Hotwire Turbo for modern web interactions</li>
        \\        </ul>
        \\    </div>
        \\</body>
        \\</html>
    ;

    return try createResponse(allocator, 200, "text/html", html);
}

fn createResponse(allocator: Allocator, status: u16, content_type: []const u8, body: []const u8) ![]const u8 {
    const status_text = switch (status) {
        200 => "OK",
        404 => "Not Found",
        else => "Unknown",
    };

    return try std.fmt.allocPrint(
        allocator,
        "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\nConnection: close\r\n\r\n{s}",
        .{ status, status_text, content_type, body.len, body },
    );
}


Pretty much on the right track.

Browsers are notoriously stringent about data adhering to standards, and will not display responses if they step out of line.

not on the computer, so can’t check. If you do curl -v it should show you this happening (or better, use telnet or nc to see the raw traffic)

Other than that, the flow looks ok to me

Listen → accept → read header → send response

given that you seem to be using zig 0.16 here, note that there is a std.http.Server type that does all this grunt work for you. The flow for that is

listen → accept → pass that accepted connection to std.http.Server.init, and from there it will handle processing headers, transfer encoding, and a whole pile of gotchas for you. If you use that stdlib feature, you don’t need a lot of the fiddly code there.

Side note - the bit where you are splitting up the response into “chunks” that fit the size of the write buffer, you don’t need to do all that. That buffer on the writer is used internally for it to work out when it’s full, and break it down into flush() calls automatically. You can happily write 100k to that writer with the 8k buffer, and it will handle it correctly, no probs.

3 Likes

The reason that the browser does not respond is because the call to reader.interface.readSliceShort blocks until EndOfStream is received (or until the buffer is full).
But the browser (and curl) does not close the connection after sending the request, to keep the connection alive for next requests.

To solve this, instead of looking for the end of the stream, you have to detect the end of the request by the sequence \r\n\r\n.

One way that works is by replacing

const bytes_read = reader.interface.readSliceShort(&buffer) catch {
    continue;
};

if (bytes_read == 0) {
    continue;
}

const request = buffer[0..bytes_read];

with

// Create a dynamic array to store the HTTP request
var received: std.ArrayList(u8) = .empty;
defer received.deinit(allocator);
  
while (!std.mem.endsWith(u8, received.items, "\r\n\r\n")) {
    const char = reader.interface.takeByte() catch {
        // Unexpected connection close
        break; // TODO: abort handling this connection by continuing the main while loop
    };
    try received.append(allocator, char);
}
  
if (received.items.len == 0) {
    continue;
}
  
const request = received.items;

This code reads the incoming data byte by byte and adds it to a dynamic array, until it encounters \r\n\r\n.

3 Likes

Thanks. I will look into the unnecessary chunking.

Thanks. It worked.