http.zig (httpz)

repository·master·Indexed 23 days ago

https://github.com/karlseguin/http.zig

A high-performance HTTP/1.1 server implementation for Zig designed to be faster than std.http.Server. It features a flexible handler system for application state, a router with support for path parameters and globs, middleware implementation, and comprehensive request/response utilities including JSON parsing, lazy loading for large bodies, and custom dispatch logic.

Tokens
9.4K
Snippets
30
Records
36
Agent score
32%

What's inside http.zig

  1. Manage memory for responses using arenas or writers

    master

    Any memory allocated for a response (like the body or headers) must remain valid until after the action returns. You have two primary ways to handle this:

    1. Use res.arena: This is a fast, thread-local buffer that falls back to an std.heap.ArenaAllocator. It is the recommended first option for data that only needs to live until the action exits.
    2. Use res.writer(): You can write directly to the response stream. Once res.write() returns, the response is sent and you can safely clean up resources.

    Note: When using res.writer(), you must provide a buffer (e.g., &.{}) to align with the *std.Io.Writer interface in Zig 0.15.

    fn arenaExample(req: *httpz.Request, res: *httpz.Response) !void {
        const query = try req.query();
        const name = query.get("name") orelse "stranger";
        res.body = try std.fmt.allocPrint(res.arena, "Hello {s}", .{name});
    }
    
    fn writerExample(req: *httpz.Request, res: *httpz.Response) !void {
        const query = try req.query();
        const name = query.get("name") orelse "stranger";
        try std.fmt.format(res.writer(&.{}), "Hello {s}", .{name});
    }
  2. Use Per-Request Context for request-specific data

    master

    If you need to pass data that is unique to a single request (like a loaded User object) to your handlers, use a RequestContext struct. You can implement this by having your dispatch method create a new context instance for every request and then calling the action with that context.

    httpz automatically infers the type of the action based on the second parameter of your dispatch method.

    const RequestContext = struct {
      app: *App,
      user: ?User,
    };
    
    const App = struct {
      pub fn dispatch(self: *App, action: httpz.Action(*RequestContext), req: *httpz.Request, res: *httpz.Response) !void {
        var ctx = RequestContext{
          .app = self,
          .user = self.loadUser(req),
        };
        return action(&ctx, req, res);
      }
    
      fn loadUser(self: *App, req: *httpz.Request) ?User {
        // logic to load user from header
      }
    };
    
    fn getUser(ctx: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
       // ctx.user is available here
    }
  3. Understand Blocking Mode vs Non-Blocking Mode

    master

    httpz uses epoll (Linux) or kqueue (macOS/BSD) for non-blocking I/O. On other platforms (like Windows), it uses a blocking mode (thread-per-connection).

    You can check the current mode using httpz.blockingMode().

    Key differences in Blocking Mode:

    • Worker Count: config.workers.count is hard-coded to 1. Any additional workers configured are added to the thread_pool.count instead.
    • Connection Limits: config.workers.max_conn and config.workers.min_conn are ignored. The maximum number of connections is limited by the size of the thread_pool.
    • Buffer Pooling: config.workers.large_buffer_count defaults to the size of the thread pool.
    • Security: Blocking mode is more susceptible to DOS attacks. It is highly recommended to run httpz behind a reverse proxy (like NGINX) when in blocking mode.
  4. Use route parameters and globs

    master

    Parameters

    Use :CAPTURE_NAME in the path. Access them via req.params.get("name").

    Globs

    You can use * to match a single segment or /* to match a suffix. When multiple globs exist, the most specific route is selected.

    Note: Routes must be lowercase. Parameter names can use any casing, but you must use the exact same casing when retrieving them.

  5. Use a custom Handler to share application state

    master

    Instead of using void as the generic type for httpz.Server, you can pass a custom struct (a "Handler") to init. This struct instance is then passed as the first argument to every action/route handler, allowing you to share resources like database connection pools across all requests.

    const App = struct {
        db: *pg.Pool,
    };
    
    // The first argument is now *App instead of *httpz.Request
    fn getUser(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
      const user_id = req.param("id").?;
      // use app.db ...
    }
    
    // Initialization
    var app = App{ .db = db };
    var server = try httpz.Server(*App).init(init.io, allocator, .{.address = .localhost(5882)}, &app);
    var router = try server.router(.{});
    router.get("/api/user/:id", getUser, .{});
  6. Quickstart: Create a basic HTTP server

    master

    To start a basic server, initialize httpz.Server(void) and use a router to define routes. The server blocks on server.listen().

    const std = @import("std");
    const httpz = @import("httpz");
    
    pub fn main(init: std.process.Init) !void {
      const allocator = init.gpa;
    
      var server = try httpz.Server(void).init(init.io, allocator, .{{
        .address = .localhost(5882),
      }}, {});
      defer {
        server.stop();
        server.deinit();
      }
    
      var router = try server.router(.{});
      router.get("/api/user/:id", getUser, .{});
    
      try server.listen();
    }
    
    fn getUser(req: *httpz.Request, res: *httpz.Response) !void {
      res.status = 200;
      try res.json(.{.id = req.param("id").?, .name = "Teg"}, .{});
    }
  7. Install http.zig via zig fetch and build.zig

    master

    To use httpz in your Zig project, follow these two steps:

    1. Add the dependency to your build.zig.zon using zig fetch:

      zig fetch --save "git+https://github.com/karlseguin/http.zig#master"

      Note: Use the appropriate branch if you are not using Zig master (e.g., zig-0.15).

    2. In your build.zig, add the httpz module as a dependency to your executable:

      const httpz = b.dependency("httpz", .{
          .target = target,
          .optimize = optimize,
      });
      
      exe.root_module.addImport("httpz", httpz.module("httpz"));
    zig fetch --save "git+https://github.com/karlseguin/http.zig#master"
    
    const httpz = b.dependency("httpz", .{
        .target = target,
        .optimize = optimize,
    });
    
    exe.root_module.addImport("httpz", httpz.module("httpz"));
  8. Implement and use Middlewares

    master

    A middleware is a struct that implements a Config type, an init function, and an execute method (and optionally a deinit method).

    1. Create the middleware instance using server.middleware(MiddlewareStruct, config).
    2. Apply it globally via server.router(.{.middlewares = &.{middleware}}).
    3. Apply it to specific routes via the route configuration.
    4. Use middleware_strategy = .replace if you want the route's middleware to override the global ones instead of appending to them.
    const cors = try server.middleware(httpz.middleware.Cors, .{
      .origin = "https://www.openmymind.net/",
    });
    
    // Global application
    var router = try server.router(.{.middlewares = &.{cors}});
    
    // Route-specific application
    router.get("/v1/users", user, .{.middlewares = &.{cors}});
    
    // Replacing global middleware for a specific route
    router.get("/v1/metrics", metrics, .{.middlewares = &.{cors}, .middleware_strategy = .replace});
  9. Implement Server-Side Events (SSE)

    master

    To enable Server-Side Events, call res.startEventStream(context, handler_function).

    Requirements & Behavior:

    • The handler_function is executed in a new thread.
    • The function receives the provided context and an std.net.Stream.
    • You must add any necessary headers (like Content-Type) via res.headers.add before calling startEventStream().
    • Crucial: Do not set res.body (directly or indirectly) after starting the stream.
    • startEventStream() automatically sets Content-Type, Cache-Control, and Connection headers.
    fn handler(_: *Request, res: *Response) !void {
        try res.startEventStream(StreamContext{}, StreamContext.handle);
    }
    
    const StreamContext = struct {
        fn handle(self: StreamContext, stream: std.net.Stream) void {
            while (true) {
                stream.writeAll("event: ....") catch return;
            }
        }
    }
  10. Expose Prometheus-compatible metrics

    master

    The library collects basic operational metrics using metrics.zig. You can expose these metrics by writing them to an std.io.Writer (such as the response writer in a handler) using httpz.writeMetrics(writer).

    Warning: httpz does not provide built-in authorization. Ensure you protect the endpoint exposing these metrics to prevent unauthorized access to server internals.

    pub fn metrics(_: *httpz.Request, res: *httpz.Response) !void {
        const writer = res.writer();
        try httpz.writeMetrics(writer);
    }
  11. Test HTTP requests and responses with httpz.testing

    master

    The httpz.testing namespace provides a testing utility to simulate requests and assert responses without running a full server.

    Use ht.init(.{}) to create a test context. This returns a structure that provides access to a mock *httpz.Request and *httpz.Response.

    const ht = @import("httpz").testing;
    
    test "example test" {
        var web_test = ht.init(.{});
        defer web_test.deinit();
    
        // ... perform actions ...
    }
  12. Use lazy loading for large request bodies

    master

    By default, httpz reads the full request body into memory. If config.request.lazy_read_size is set, bodies larger than that size are not fully loaded.

    Instead, you can stream the body using req.reader(timeout_in_ms). This returns an io.Reader that abstracts whether the data is already in memory or still arriving on the socket. You can check req.unread_body > 0 to determine if lazy loading is active.

    // 5000 millisecond read timeout on a per-read basis
    var reader = try req.reader(5000);
    var buf: [4096]u8 = undefined;
    while (true) {
        const n = try reader.read(&buf);
        if (n == 0) break
       // buf[0..n] is what was read
    }