SkillAgentSearch skills...

Http.zig

An HTTP/1.1 server for zig

Install / Use

/learn @karlseguin/Http.zig
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

An HTTP/1.1 server for Zig.

const std = @import("std");
const httpz = @import("httpz");

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

  // More advance cases will use a custom "Handler" instead of "void".
  // The last parameter is our handler instance, since we have a "void"
  // handler, we passed a void ({}) value.
  var server = try httpz.Server(void).init(allocator, .{
    // use .all(5882) to bind to all interfaces, i.e. 0.0.0.0
    .address = .localhost(5882),
  }, {});
  defer {
    // clean shutdown, finishes serving any live request
    server.stop();
    server.deinit();
  }

  var router = try server.router(.{});
  router.get("/api/user/:id", getUser, .{});

  // blocks
  try server.listen();
}

fn getUser(req: *httpz.Request, res: *httpz.Response) !void {
  res.status = 200;
  try res.json(.{.id = req.param("id").?, .name = "Teg"}, .{});
}

Table of Contents

Versions

The master branch targets the latest stable of Zig (0.15.1). The dev branch targets the latest version of Zig. If you're looking for an older version, look for an zig-X.YZ branches.

Examples

See the examples folder for examples. If you clone this repository, you can run zig build example_# to run a specific example:

$ zig build example_1
listening http://localhost:8800/

Installation

  1. Add http.zig as a dependency in your build.zig.zon:
zig fetch --save "git+https://github.com/karlseguin/http.zig#master"
  1. In your build.zig, add the httpz module as a dependency to your program:
const httpz = b.dependency("httpz", .{
    .target = target,
    .optimize = optimize,
});

// the executable from your call to b.addExecutable(...)
exe.root_module.addImport("httpz", httpz.module("httpz"));

The library tracks Zig master. If you're using a specific version of Zig, use the appropriate branch.

Alternatives

If you're looking for a higher level web framework with more included functionality, consider JetZig or Tokamak which are built on top of httpz.

Why not std.http.Server

std.http.Server is very slow and assumes well-behaved clients.

There are many Zig HTTP server implementations. Most wrap std.http.Server and tend to be slow. Benchmark it, you'll see. A few wrap C libraries and are faster (though some of these are slow too!).

http.zig is written in Zig, without using std.http.Server. On an M2, a basic request can hit 140K requests per seconds.

Handler

When a non-void Handler is used, the value given to Server(H).init is passed to every action. This is how application-specific data can be passed into your actions.

For example, using pg.zig, we can make a database connection pool available to each action:

const pg = @import("pg");
const std = @import("std");
const httpz = @import("httpz");

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

  var db = try pg.Pool.init(allocator, .{
    .connect = .{ .port = 5432, .host = "localhost"},
    .auth = .{.username = "user", .database = "db", .password = "pass"}
  });
  defer db.deinit();

  var app = App{
    .db = db,
  };

  var server = try httpz.Server(*App).init(allocator, .{.address = .localhost(5882)}, &app);
  var router = try server.router(.{});
  router.get("/api/user/:id", getUser, .{});
  try server.listen();
}

const App = struct {
    db: *pg.Pool,
};

fn getUser(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
  const user_id = req.param("id").?;

  var row = try app.db.row("select name from users where id = $1", .{user_id}) orelse {
    res.status = 404;
    res.body = "Not found";
    return;
  };
  defer row.deinit() catch {};

  try res.json(.{
    .id = user_id,
    .name = row.get([]u8, 0),
  }, .{});
}

Custom Dispatch

Beyond sharing state, your custom handler can be used to control how httpz behaves. By defining a public dispatch method you can control how (or even if) actions are executed. For example, to log timing, you could do:

const App = struct {
  pub fn dispatch(self: *App, action: httpz.Action(*App), req: *httpz.Request, res: *httpz.Response) !void {
    var timer = try std.time.Timer.start();

    // your `dispatch` doesn't _have_ to call the action
    try action(self, req, res);

    const elapsed = timer.lap() / 1000; // ns -> us
    std.log.info("{} {s} {d}", .{req.method, req.url.path, elapsed});
  }
};

Per-Request Context

The 2nd parameter, action, is of type httpz.Action(*App). This is a function pointer to the function you specified when setting up the routes. As we've seen, this works well to share global data. But, in many cases, you'll want to have request-specific data.

Consider the case where you want your dispatch method to conditionally load a user (maybe from the Authorization header of the request). How would you pass this User to the action? You can't use the *App directly, as this is shared concurrently across all requests.

To achieve this, we'll add another structure called RequestContext. You can call this whatever you want, and it can contain any fields of methods you want.

const RequestContext = struct {
  // You don't have to put a reference to your global data.
  // But chances are you'll want.
  app: *App,
  user: ?User,
};

We can now change the definition of our actions and dispatch method:

fn getUser(ctx: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
   // can check if ctx.user is != null
}

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 {
    // todo, maybe using req.header("authorizaation")
  }
};

httpz infers the type of the action based on the 2nd parameter of your handler's dispatch method. If you use a void handler or your handler doesn't have a dispatch method, then you won't interact with httpz.Action(H) directly.

Not Found

If your handler has a public notFound method, it will be called whenever a path doesn't match a found route:

const App = struct {
  pub fn notFound(_: *App, req: *httpz.Request, res: *httpz.Response) !void {
    std.log.info("404 {} {s}", .{req.method, req.url.path});
    res.status = 404;
    res.body = "Not Found";
  }
};

Error Handler

If your handler has a public uncaughtError method, it will be called whenever there's an unhandled error. This could be due to some internal httpz bug, or because your action return an error.

const App = struct {
  pub fn uncaughtError(_: *App, req: *httpz.Request, res: *httpz.Response, err: anyerror) void {
    std.log.info("500 {} {s} {}", .{req.method, req.url.path, err});
    res.status = 500;
    res.body = "sorry";
  }
};

Notice that, unlike notFound and other normal actions, the uncaughtError method cannot return an error itself.

Takeover

For the most control, you can define a handle method. This circumvents most of Httpz's dispatching, including routing. Frameworks like JetZig hook use handle in order to provide their own routing and dispatching. When you define a handle method, then any dispatch, notFound and uncaughtError methods are ignored by httpz.

const App = struct {
  pub fn handle(app: *App, req: *httpz.Request, res: *httpz.Response) void {
    // todo
  }
};

The behavior httpz.Server(H) is controlled by The library supports both simple and complex use cases. A simple use case is shown below. It's initiated by the call to httpz.Server():

const std = @import("std");
const httpz = @import("httpz");

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

    var server = try httpz.Server(void).init(allocator, .{.address = .localhost(5882)}, {});

    // overwrite the default notFound handler
    server.notFound(notFound);

    // overwrite the default error handler
    server.errorHandler(errorHandler);

    var router = try server.router(.{});

    // use get/post/put/head/patch/options/delete
    // you can also use "all" to attach to all methods
    router.get("/api/user/:id", getUser, .{});

    // start the server in the current thread, blocking.
    try server.listen();
}

fn getUser(req: *httpz.Request, res: *httpz.Response) !void {
    // status code 200 is implicit.

    // The json helper will automatically set the res.content_type = httpz.ContentType.JSON;
    // Here we're passing an inferred anonymous structure, but you can pass anytype
    // (so long as it can be serialized using std.json.stringify)

    try res.json(.{.id = req.param("id").?, .name = "Teg"}, .{});
}

fn notFound(_: *httpz.Request, res: *httpz.Response) !void {
    res.status = 404;

    // you can set the body directly to a []u8, but note that the memory
    // must be valid beyond your handler. Use the r
View on GitHub
GitHub Stars1.4k
CategoryDevelopment
Updated1d ago
Forks100

Languages

Zig

Security Score

100/100

Audited on Mar 26, 2026

No findings