Warning
Still work in progress.
- π Cross-platform support via Zig std.Io backends
- πΎ Zero runtime allocations - all memory allocated at compile time
- π Lock-free atomic operations for maximum concurrency
- π§© Bring your own router - plug in any dispatcher or router you want
- β HTTP/2 RFC 7540 compliant
Add http2.zig to your build.zig.zon:
.{
.name = "my-project",
.version = "1.0.0",
.dependencies = .{
.http2 = .{
.url = "https://github.com/hendriknielaender/http2.zig/archive/main.tar.gz",
.hash = "1220...", // Use `zig fetch` to get the hash
},
},
}Import in your build.zig:
const http2_dep = b.dependency("http2", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("http2", http2_dep.module("http2"));const std = @import("std");
const http2 = @import("http2");
fn handleRequest(ctx: *const http2.Context) !http2.Response {
if (ctx.method == .get) {
if (std.mem.eql(u8, ctx.path, "/")) {
return ctx.response.text(.ok, "hello from http2.zig\n");
}
}
return ctx.response.text(.not_found, "not found\n");
}
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Initialize the HTTP/2 system
try http2.init(allocator);
defer http2.deinit();
// Configure and create server
const config = http2.Server.Config{
.address = try std.Io.net.IpAddress.parse("127.0.0.1", 3000),
.dispatcher = http2.RequestDispatcher.fromHandler(handleRequest),
};
var server = try http2.Server.init(allocator, config);
defer server.deinit();
std.log.info("HTTP/2 server listening on {f}", .{config.address});
// Run the server
try server.run();
}The core library stays router-agnostic. If you want radix-tree routing, path params, and HTTP
helpers, add turboapi-core to your application:
zig fetch --save=turboapi_core "git+https://github.com/justrach/turboapi-core.git#main"Wire it into build.zig:
const core_dep = b.dependency("turboapi_core", .{
.target = target,
.optimize = optimize,
});
const core_mod = core_dep.module("turboapi-core");
exe.root_module.addImport("turboapi-core", core_mod);Then bridge your router into http2.zig with a typed dispatcher:
const std = @import("std");
const core = @import("turboapi-core");
const http2 = @import("http2");
const App = struct {
router: core.Router,
fn init(target: *App, allocator: std.mem.Allocator) !void {
target.* = .{
.router = core.Router.init(allocator),
};
errdefer target.deinit();
try target.router.addRoute("GET", "/", "index");
try target.router.addRoute("GET", "/users/{id}", "user_show");
}
fn deinit(self: *App) void {
self.router.deinit();
}
fn dispatch(self: *const App, ctx: *const http2.Context) !http2.Response {
if (self.router.findRoute(ctx.method.bytes(), ctx.path)) |match_result| {
var match = match_result;
defer match.deinit();
if (std.mem.eql(u8, match.handler_key, "index")) {
return ctx.response.text(.ok, "hello\n");
}
if (std.mem.eql(u8, match.handler_key, "user_show")) {
_ = match.params.get("id");
return ctx.response.text(.ok, "user\n");
}
}
return ctx.response.text(.not_found, "not found\n");
}
};
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
try http2.init(allocator);
defer http2.deinit();
var app: App = undefined;
try App.init(&app, allocator);
defer app.deinit();
const config = http2.Server.Config{
.address = try std.Io.net.IpAddress.parse("127.0.0.1", 3000),
.dispatcher = http2.RequestDispatcher.bind(App, &app, App.dispatch),
};
var server = try http2.Server.init(allocator, config);
defer server.deinit();
try server.run();
}Repository examples:
examples/basic_tls.zigshows the same dispatcher API with a small custom Zig router over TLS.examples/turboapi.zigshows theturboapi-coreintegration over TLS.
The core module does not own TLS or link BoringSSL. A TLS adapter package, such as
http2-boring, configures BoringSSL, completes the TLS handshake, verifies ALPN h2, and then
passes the decrypted application-data stream into the core connection entry point:
try http2.serveConnection(
allocator,
tls_conn.reader(),
tls_conn.writer(),
.{
.dispatcher = dispatcher,
},
);The repository examples and benchmarks use this shape through examples/tls_server.zig;
the core library target remains TLS-provider agnostic.
By default, the build fetches the boring release package and loads
http2-boring from that package. Override it with
-Dhttp2-boring-root=/path/to/http2-boring when testing a local adapter checkout.
The boring release source archive does not include the BoringSSL submodule, so
this build passes the local boringssl checkout as its source by default; use
-Dboringssl-source-path=/path/to/boringssl to point at another checkout.
For pooled servers, the adapter can pass caller-owned stream storage through
http2.ServeConnectionOptions.stream_storage to avoid per-connection stream-storage allocation.
TBD
pub const Server.Config = struct {
/// Address to bind to
address: std.Io.net.IpAddress,
/// Request dispatcher for application routing or request handling
dispatcher: RequestDispatcher,
/// Maximum concurrent connections (default: 1000)
max_connections: u32 = 1000,
/// Buffer size per connection (default: 32KB)
buffer_size: u32 = 32 * 1024,
};http2.zig no longer ships with a built-in router. Instead, Server.Config takes a
RequestDispatcher, which is just a function pointer plus optional typed state.
For stateless handling:
.dispatcher = http2.RequestDispatcher.fromHandler(handleRequest),For stateful apps, middleware stacks, or third-party routers:
.dispatcher = http2.RequestDispatcher.bind(App, &app, App.dispatch),This keeps transport, request parsing, and response building inside http2.zig, while letting the
application decide how routing, params, middleware, and fallback behavior should work.
The request context passed to handlers exposes:
ctx.methodctx.pathctx.queryctx.headersctx.bodyctx.response
// Create a new server
pub fn init(allocator: Allocator, config: Config) !Server
// Clean up server resources
pub fn deinit(self: *Server) void
// Run the server event loop (blocks)
pub fn run(self: *Server) !void
// Stop the server
pub fn stop(self: *Server) void
// Get server statistics
pub fn getStats(self: *Server) ServerStatspub const ServerStats = struct {
total_connections: u64,
active_connections: u32,
requests_processed: u64,
};- Zig v0.16.0
# Build the library
zig build
# Run tests
zig build test
# Build with optimizations
zig build -Doptimize=ReleaseFast# Generate local cert.pem/key.pem if needed
make cert
# Run the basic HTTP/2 over TLS example with the local Zig router
zig build run
# Run the turboapi-core example
zig build run-turboapi
# Run the benchmark server
zig build benchmarkcd benchmarks
./bench.shhttp2.zig implements core HTTP/2 features:
- β HTTP/2 Connection Preface
- β Binary Frame Protocol
- β Stream Multiplexing
- β Flow Control
- β HPACK Header Compression
- β Error Handling with GOAWAY frames
- β SETTINGS frame exchange
- β PING frame handling
Contributions are welcome! Please ensure:
- All tests pass
- No runtime allocations are introduced
- Performance benchmarks show no regression
Areas for contribution:
- Additional frame type implementations
- Enhanced HPACK optimization
- More comprehensive examples
- Performance improvements
MIT License - see LICENSE for details.
- Built with Zig 0.16 std.Io
- Inspired by TigerBeetle's zero-allocation principles
- HTTP/2 Specification - RFC 7540
- HPACK Specification - RFC 7541
