Zero-allocation HTTP/1.1, HTTP/2, and HTTP/3 protocol library for Zig. All parsing and formatting operates on caller-provided buffers
Zig 0.15.0+.
zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const libhttp = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("http", libhttp.module("libhttp"));The library is organized in four layers:
┌─────────────────────────────────────────────┐
│ Public API (http.Server, http.Client) │
├──────────┬──────────┬───────────────────────┤
│ H1 │ H2 │ H3 │
│ proto/ │ proto/ │ proto/ │
│ h1/ │ h2/ │ h3/ + QUIC vtable │
├──────────┴──────────┴───────────────────────┤
│ Common (headers, status, URI, cookies, │
│ redirects, encoding, cache, ranges, ETags) │
├─────────────────────────────────────────────┤
│ TLS (config types, backend contract, │
│ passthrough backend for testing) │
└─────────────────────────────────────────────┘
Zero allocation. All functions write into caller-provided []u8 buffers and return slices. No std.mem.Allocator is ever required
Zero-copy parsing. Parsed struct fields are []const u8 slices pointing directly into the input buffer
Format-agnostic I/O. H1/H2 take *std.Io.Reader / *std.Io.Writer. H3 takes a Quic vtable. TLS backends produce these same interfaces
http.Method, http.Status, http.Header, http.Uri, http.Version, http.DateTime
http.Server, http.Client
http.Request, http.Response, http.Body
http.h1.* -- H1 connection, parser, scanner, chunked codec, I/O
http.h2.* -- H2 connection, HPACK, flow control, streams, priorities
http.h3.* -- H3 connection, QPACK, frames, QUIC interface
http.common.* -- headers, URI, media types, auth, cache-control, ranges, ETags,
quality values, cookies, redirects, content-encoding, timeouts
http.tls.* -- TLS config, backend contract, passthrough backend
http.proxy.* -- intermediary header rewriting, Via, request/response classification
http.cache.* -- canStore, evaluateFreshness, merge304Headers
http.alpn.* -- ALPN token helpers for TLS negotiation
http.ConnectionPool(T) -- fixed-capacity client connection pool
http.BufferPool(N, Size) -- fixed-capacity buffer pool
const http = @import("http");
var server = http.Server.init(&reader, &writer, .{});
var head_buf: [4096]u8 = undefined;
var header_buf: [32]http.Header = undefined;
const req = try server.recvRequest(&head_buf);
if (req.expect_continue)
try server.send100Continue(&head_buf);
try server.sendResponse(&head_buf, &header_buf, .ok, null, req,
&.{.{ .name = "Content-Type", .value = "text/plain" }},
"hello", .{});
if (server.shouldClose()) break;var body_buf: [65536]u8 = undefined;
const n = try server.recvRequestBody(&body_buf, req);
const body = body_buf[0..n];var body_buf: [65536]u8 = undefined;
var trailer_block: [1024]u8 = undefined;
var trailer_hdrs: [8]http.Header = undefined;
const result = try server.recvRequestBodyWithTrailers(
&body_buf, &trailer_block, &trailer_hdrs, req);
const body = body_buf[0..result.body_len];
const trailers = result.trailers;while (true) {
const req = try server.recvRequest(&head_buf);
// ... process ...
try server.sendResponse(&head_buf, &header_buf, .ok, null, req, &.{}, body, .{});
if (server.shouldClose()) break;
}server.conn.initiateGracefulClose();
// shouldClose() now returns true, next recvRequest returns ConnectionClosingconst req = try server.recvRequest(&head_buf);
if (req.upgrade_requested) {
try server.sendResponse(&head_buf, &header_buf, .switching_protocols, null, req,
&.{.{ .name = "Upgrade", .value = "websocket" },
.{ .name = "Connection", .value = "Upgrade" }}, null, .{});
var adapter = server.takeRequestUpgradeAdapter(req).?;
// adapter.read() / adapter.write() for raw transport
}if (req.connect_requested) {
try server.sendResponse(&head_buf, &header_buf, .ok, null, req, &.{}, null, .{});
var adapter = server.takeRequestTunnelAdapter(req).?;
// bidirectional tunnel
}var client = http.Client.init(&reader, &writer, .{});
var head_buf: [4096]u8 = undefined;
var header_buf: [32]http.Header = undefined;
const sent = try client.sendRequest(&head_buf, &header_buf,
"GET",
try http.RequestTarget.parse("http://example.com/resource"),
.@"HTTP/1.1",
&.{}, null, .{});
const resp = try client.recvResponse(&head_buf, sent);
var body_buf: [65536]u8 = undefined;
const n = try client.recvResponseBody(&body_buf, resp);const sent = try client.sendRequest(&head_buf, &header_buf,
"POST",
try http.RequestTarget.parse("/api/data"),
.@"HTTP/1.1",
&.{.{ .name = "Content-Type", .value = "application/json" }},
"{\"key\":\"val\"}",
.{});const h2 = http.h2;
var conn = h2.ServerConnection.init(&reader, &writer, options, &streams, &decoder, &encoder);
try conn.recvPreface(&frame_buf);
while (true) {
const event = try conn.recvFrame(&frame_buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf);
switch (event) {
.headers => |h| {
// h.stream_id, h.headers, h.end_stream
try conn.sendHeaders(&frame_buf, h.stream_id, &response_headers, true);
},
.data => |d| {
// d.stream_id, d.data, d.end_stream
},
.goaway => break,
else => {},
}
}try conn.initiateGracefulShutdown(&frame_buf);
// Continue processing existing streams...
if (conn.isFullyDrained()) break;const p = try http.h2.Priority.parse("u=0, i");
// p.urgency = 0, p.incremental = true
var buf: [16]u8 = undefined;
const formatted = try p.format(&buf); // "u=0, i"H3 uses a QUIC transport interface instead of Reader/Writer. You provide a QUIC implementation via the Quic vtable - the library does not include a QUIC stack.
const h3 = http.h3;
// Provide your QUIC implementation through the vtable.
const quic = h3.Quic{ .ptr = @ptrCast(my_quic_conn), .vtable = &MyQuic.vtable };
var conn = h3.ServerConnection.init(quic, .{}, &streams, &decoder, &encoder);
try conn.handshake(&buf);
while (true) {
const event = try conn.recvEvent(&buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf);
switch (event) {
.headers => |h| {
try conn.sendHeaders(&buf, h.stream_id, &response_headers, true);
},
.data => |d| {
// d.stream_id, d.data, d.end_stream
},
.goaway => break,
else => {},
}
}var conn = h3.ClientConnection.init(quic, .{}, &streams, &decoder, &encoder);
try conn.handshake(&buf);
const stream_id = try conn.sendHeaders(&buf, &request_headers, false);
try conn.sendData(&buf, stream_id, request_body, true);
const event = try conn.recvEvent(&buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf);const MyQuic = struct {
// ... your QUIC state ...
const vtable = h3.Quic.VTable{
.read = readFn,
.write = writeFn,
.openBidiStream = openBidiFn,
.openUniStream = openUniFn,
.acceptBidiStream = acceptBidiFn,
.acceptUniStream = acceptUniFn,
.resetStream = resetFn,
.stopStream = stopFn,
.closeConnection = closeFn,
};
};The library provides TLS configuration types and a pluggable backend interface. Actual TLS implementations (OpenSSL, BoringSSL, std.crypto.tls) are external - the library defines the contract.
// Client TLS config
const client_cfg = http.tls.ClientConfig{
.server_name = "example.com",
.alpn_protocols = http.alpn.default_alpn_list,
.verify_server_cert = true,
};
// Server TLS config
const server_cfg = http.tls.ServerConfig{
.cert_key = .{ .cert_pem = cert_bytes, .key_pem = key_bytes },
.alpn_protocols = http.alpn.default_alpn_list,
};var session: http.tls.none.Session = .{};
const result = try http.tls.none.initClient(&session, &reader, &writer, client_cfg);
// result.reader, result.writer - same as transport (no encryption)
// result.negotiated_protocol - first ALPN tokenImplement three functions and a Session type:
const MyTls = struct {
const Session = struct {
// Your TLS session state (OpenSSL SSL*, std.crypto.tls.Client, etc.)
// Embed Io.Reader/Writer to produce them from TLS reads/writes.
};
fn initClient(session: *Session, reader: *Io.Reader, writer: *Io.Writer,
cfg: http.tls.ClientConfig) http.tls.HandshakeError!http.tls.HandshakeResult {
// Perform TLS handshake, return wrapped reader/writer
}
fn initServer(session: *Session, reader: *Io.Reader, writer: *Io.Writer,
cfg: http.tls.ServerConfig) http.tls.HandshakeError!http.tls.HandshakeResult {
// Perform TLS handshake for server side
}
fn deinit(session: *Session) void {
// Clean up TLS resources
}
// Validate at compile time:
comptime { http.tls.validateBackend(@This()); }
};// 1. Configure ALPN
const cfg = http.tls.ClientConfig{
.server_name = "example.com",
.alpn_protocols = http.alpn.default_alpn_list, // "h2", "http/1.1"
};
// 2. Perform TLS handshake (with your backend)
const result = try MyTls.initClient(&session, &reader, &writer, cfg);
// 3. Select protocol based on negotiation
switch (result.negotiated_protocol) {
.h2 => {
var server = http.Server.initH2(result.reader, result.writer, ...);
},
.http_1_1 => {
var server = http.Server.init(result.reader, result.writer, .{});
},
.h3 => unreachable, // H3 uses QUIC, not TCP+TLS
.unknown => return error.UnsupportedProtocol,
}const input = "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n";
const req = try http.h1.RequestHead.parse(input);
req.method // .GET
req.uri // "/index.html"
req.version // .@"HTTP/1.1"
req.host // "example.com"
req.content_length // ?u64const input = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
const resp = try http.h1.ResponseHead.parse(input);
resp.status // .ok
resp.content_length // 5var it = req.iterateHeaders(head_bytes);
while (it.next()) |hdr| {
// hdr.name, hdr.value -- slices into head_bytes
}if (http.WellKnown.match("Content-Type")) |wk| {
const canonical = wk.toCanonical(); // "Content-Type"
}
// Includes cookie headers:
if (http.WellKnown.match("Set-Cookie")) |wk| {
// wk == .set_cookie
}var head_buf: [8192]u8 = undefined;
const result = try http.h1.recvHead(&reader, &head_buf);
const head = try http.h1.RequestHead.parse(head_buf[0..result.head_end]);
const overflow = head_buf[result.head_end..result.data_end];var head_buf: [1024]u8 = undefined;
try http.h1.sendRequestHead(&writer, &head_buf, "POST", "/api", .@"HTTP/1.1", &.{
.{ .name = "Host", .value = "example.com" },
.{ .name = "Content-Length", .value = "13" },
});
try http.h1.sendBody(&writer, "{\"key\":\"val\"}");// Sending
var chunk_buf: [256]u8 = undefined;
try http.h1.sendChunk(&writer, &chunk_buf, "first chunk");
try http.h1.sendChunk(&writer, &chunk_buf, "second chunk");
try http.h1.sendLastChunk(&writer, &chunk_buf);
// Or all at once:
try http.h1.sendBodyChunked(&writer, &chunk_buf, "entire body");
// With trailers:
try http.h1.sendLastChunkWithTrailers(&writer, &chunk_buf, &.{
.{ .name = "Checksum", .value = "abc123" },
});const body_len = http.h1.responseBodyLength(method, status, te, content_length);
var body_buf: [65536]u8 = undefined;
const n = try http.h1.recvBody(&reader, &body_buf, body_len, overflow);body_len is a tagged union: .length(n), .chunked, or .read_until_close.
For code that handles both H1 and H2:
// From H1
const req = http.Request.fromH1(received_request, head_bytes);
// From H2
const req = http.Request.fromH2(received_headers_event);
// Uniform access regardless of protocol
req.method // []const u8
req.method_enum // ?Method
req.target // path (H1) or :path (H2)
req.authority // Host header (H1) or :authority (H2)
req.protocol // .h1 or .h2
req.body // .none, .content_length, .chunked, .streamvar resp = http.Response.init(.ok);
_ = resp.setContentType("application/json");
var len_buf: [20]u8 = undefined;
_ = resp.setContentLength(1234, &len_buf);
_ = resp.setHeader("Cache-Control", "no-cache");
// resp.headers() returns []const Headerconst sc = try http.common.SetCookie.parse(
"sid=abc123; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Lax"
);
// sc.name = "sid", sc.value = "abc123"
// sc.domain = "example.com", sc.secure = true, sc.http_only = true
// sc.same_site = .laxvar it = http.common.CookieIterator.init("id=abc; theme=dark; lang=en");
while (it.next()) |pair| {
// pair.name, pair.value
}if (http.common.findCookie(cookie_header, "session_id")) |value| {
// use value
}var buf: [256]u8 = undefined;
if (sc.format(&buf)) |header_value| {
_ = resp.setHeader("Set-Cookie", header_value);
}const redirect = @import("http").common;
// Classify a status code
if (redirect.classifyRedirect(.moved_permanently)) |rt| {
rt.isPermanent() // true
rt.preservesMethod() // false
}
// Determine method after redirect
const new_method = redirect.redirectMethod(.POST, .see_other);
// new_method == .GET (303 always changes to GET)
// Check if safe to auto-follow
redirect.isSafeToAutoFollow(.GET, .temporary_preserves_method) // true
redirect.isSafeToAutoFollow(.POST, .permanent_allows_method_change) // false
// Extract Location header
if (redirect.extractLocation(resp.headers())) |location| {
// follow redirect to location
}Header-level negotiation only - actual compression/decompression is external.
// Parse Content-Encoding
var it = http.common.ContentEncodingIterator.init("gzip, br");
while (it.next()) |enc| {
// enc == .gzip, then .br
}
// Identify an encoding token
http.common.Encoding.fromString("br") // .br
http.common.Encoding.fromString("zstd") // .zstd
// Select best encoding from Accept-Encoding
const offered = [_][]const u8{ "gzip", "br", "zstd" };
if (http.common.selectEncoding(accept_encoding_header, &offered)) |idx| {
// offered[idx] is the best match
}
// Check if a specific encoding is acceptable
http.common.isAcceptable("gzip", "gzip, deflate, br") // true// Use preset defaults
const cfg = http.common.TimeoutConfig.client_default;
// connect: 30s, read: 30s, write: 30s, idle: 90s, request: 300s
const srv_cfg = http.common.TimeoutConfig.server_default;
// read: 60s, write: 60s, idle: 120s, request: 600s
// Custom configuration
const custom = http.common.TimeoutConfig{
.connect_ms = 5_000,
.read_ms = 10_000,
};
// Check expiration in your event loop
if (http.common.TimeoutConfig.isExpired(cfg.read_ms, elapsed_ms)) {
return error.Timeout;
}const proto = http.alpn.NegotiatedProtocol.fromAlpnToken(alpn_bytes);
switch (proto) {
.h2 => // use HTTP/2
.http_1_1 => // use HTTP/1.1
.h3 => // use HTTP/3
.unknown => // fallback
}
// Standard token lists for TLS configuration:
http.alpn.default_alpn_list // &.{ "h2", "http/1.1" }
http.alpn.default_alpn_list_with_h3 // &.{ "h3", "h2", "http/1.1" }
http.alpn.h2_only_alpn_list // &.{ "h2" }
http.alpn.h1_only_alpn_list // &.{ "http/1.1" }
http.alpn.h3_only_alpn_list // &.{ "h3" }// Accept header
var it = http.common.AcceptIterator.init("text/html;q=0.9, application/json");
while (it.next()) |item| {
// item.media_type, item.quality
}
// Server-driven selection
const offered = [_]http.common.MediaType{
.{ .type_ = "text", .subtype = "html" },
.{ .type_ = "application", .subtype = "json" },
};
if (http.common.selectMediaType(accept_header, &offered)) |idx| {
// offered[idx] is best match
}
// Accept-Encoding / Accept-Language
const encodings = [_][]const u8{ "gzip", "br", "deflate" };
if (http.common.selectToken(accept_encoding, &encodings)) |idx| {
// encodings[idx]
}http.common.hasCacheDirective("no-cache, max-age=3600", "no-cache") // true
http.common.getMaxAge("max-age=3600") // 3600
var it = http.common.CacheDirectiveIterator.init("private, max-age=3600");
while (it.next()) |dir| {
switch (dir) {
.max_age => |seconds| { ... },
.private => |field_names| { ... },
...
}
}var it = try http.common.RangeIterator.init("bytes=0-499, -500");
while (it.next()) |range| {
switch (range) {
.slice => |s| { ... },
.suffix => |n| { ... },
.from => |f| { ... },
}
}
// Resolve against resource size
const resolved = http.common.resolveRange(.{ .suffix = 500 }, 10000);
// resolved.?.first = 9500, resolved.?.last = 9999const etag = try http.common.ETag.parse("\"abc\"");
http.common.matchesStrong("\"xyz\", \"abc\"", etag) // true
http.common.matchesWeak("W/\"abc\"", etag) // truevar server = http.Server.init(&reader, &writer, .{
.max_header_count = 128,
.max_header_name_len = 256,
.max_header_value_len = 8192,
.max_request_line_len = 8192,
.max_head_size = 65536,
});Exceeding any limit returns error.HeaderLimitExceeded. Additionally:
- NUL and control characters in header values are rejected
- Duplicate
Transfer-Encodingheaders are rejected (request smuggling vector) Transfer-Encodingon HTTP/1.0 messages is rejected- Absolute-form URI authority must match the
Hostheader
const Pool = http.ConnectionPool(MyConn);
var entries: [64]Pool.Entry = undefined;
var pool = Pool.init(&entries);
// Reuse existing connection
if (pool.acquire("example.com", 443)) |entry| {
// use entry.conn
defer pool.release(entry);
}
// Or claim a new slot
if (pool.claim("example.com", 443)) |entry| {
entry.conn = openConnection(...);
defer pool.release(entry);
}
// Evict stale entries
_ = pool.evictIdle(now_ms, 30_000);var pool: http.BufferPool(8, 4096) = .{};
const buf = pool.acquire() orelse return error.NoBuffers;
defer pool.release(buf);
// use buf[0..4096]| Buffer | Recommended | Purpose |
|---|---|---|
| Head buffer | 8192 | HTTP/1.1 request/response head parsing |
| Body buffer | 65536 | Request/response body reads |
| H2 frame buffer | 16384 + 9 | H2 frame payload (default SETTINGS_MAX_FRAME_SIZE) |
| Chunk buffer | 256–8192 | Chunked transfer encoding overhead |
| Header array | 32–128 entries | Parsed header name/value pairs |
var conn = http.h1.ServerConnection.init(&reader, &writer, .{});
// ... process requests ...
conn.stats.messages_in // requests received
conn.stats.messages_out // responses sent// URI parsing
const uri = try http.Uri.parse("http://example.com:8080/path?q=1");
// Percent encoding
var buf: [128]u8 = undefined;
const encoded = try http.common.PercentEncoding.encode(&buf, "hello world");
// Media types
const mt = try http.common.MediaType.parse("text/html; charset=utf-8");
// Basic auth
var dec_buf: [128]u8 = undefined;
const auth = try http.common.BasicAuth.decode("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", &dec_buf);
// HTTP dates
const dt = try http.DateTime.parse("Sun, 06 Nov 1994 08:49:37 GMT");
// Status codes
const s = try http.Status.parse("404");
s.phrase() // "Not Found"
s.class() // .client_error
// Methods
const m = http.Method.fromString("POST").?;
m.safe() // false
m.idempotent() // falseUse libxev for the event loop and TCP accept. Each accepted connection spawns a thread that bridges std.net.Stream to *Io.Reader / *Io.Writer for libhttp.
Add both dependencies to build.zig.zon:
zig fetch --save git+https://github.com/lithdew/libxev
zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const xev_dep = b.dependency("libxev", .{ .target = target, .optimize = optimize });
const http_dep = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("xev", xev_dep.module("xev"));
exe.root_module.addImport("http", http_dep.module("libhttp"));const std = @import("std");
const xev = @import("xev");
const http = @import("http");
pub fn main() !void {
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const addr = std.net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);
var server = try xev.TCP.init(addr);
try server.bind(addr);
try server.listen(128);
var accept_completion: xev.Completion = undefined;
server.accept(&loop, &accept_completion, {}, onAccept);
try loop.run(.until_done);
}
fn onAccept(_: void, loop: *xev.Loop, _: *xev.Completion, result: xev.AcceptError!xev.TCP) xev.CallbackAction {
const client = result catch return .rearm;
const stream = std.net.Stream{ .handle = client.fd };
_ = std.Thread.spawn(.{}, handleConnection, .{stream}) catch return .rearm;
_ = loop;
return .rearm;
}
fn handleConnection(stream: std.net.Stream) void {
defer stream.close();
var read_buf: [8192]u8 = undefined;
var write_buf: [8192]u8 = undefined;
var net_reader = std.net.Stream.Reader.init(stream, &read_buf);
var net_writer = std.net.Stream.Writer.init(stream, &write_buf);
var server = http.Server.init(net_reader.interface(), &net_writer.interface, .{});
var head_buf: [4096]u8 = undefined;
var header_buf: [32]http.Header = undefined;
while (true) {
const req = server.recvRequest(&head_buf) catch return;
server.sendResponse(&head_buf, &header_buf, .ok, null, req,
&.{.{ .name = "Content-Type", .value = "text/plain" }},
"hello from libxev", .{}) catch return;
if (server.shouldClose()) return;
}
}Implement libhttp's TLS backend contract (http.tls.validateBackend) wrapping tls.zig to serve HTTPS. tls.zig does not currently support ALPN, so the example defaults to HTTP/1.1.
zig fetch --save git+https://github.com/tls-zig/tls.zig
zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const tls_dep = b.dependency("tls", .{ .target = target, .optimize = optimize });
const http_dep = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("tls", tls_dep.module("tls"));
exe.root_module.addImport("http", http_dep.module("libhttp"));const std = @import("std");
const Io = std.Io;
const tls = @import("tls");
const http = @import("http");
const TlsBackend = struct {
const Session = struct {
conn: tls.Connection,
read_buf: [16384]u8,
write_buf: [16384]u8,
fn reader(self: *Session) *Io.Reader {
return self.conn.reader(&self.read_buf);
}
fn writer(self: *Session) *Io.Writer {
return self.conn.writer(&self.write_buf);
}
};
fn initClient(
session: *Session,
reader: *Io.Reader,
writer: *Io.Writer,
cfg: http.tls.ClientConfig,
) http.tls.HandshakeError!http.tls.HandshakeResult {
session.conn = tls.clientFromStream(reader, writer, .{
.server_name = cfg.server_name,
}) catch return error.TlsHandshakeFailed;
// tls.zig does not support ALPN; default to HTTP/1.1.
return .{
.reader = session.reader(),
.writer = session.writer(),
.negotiated_protocol = .http_1_1,
};
}
fn initServer(
session: *Session,
reader: *Io.Reader,
writer: *Io.Writer,
cfg: http.tls.ServerConfig,
) http.tls.HandshakeError!http.tls.HandshakeResult {
session.conn = tls.serverFromStream(reader, writer, .{
.cert_pem = cfg.cert_key.cert_pem,
.key_pem = cfg.cert_key.key_pem,
}) catch return error.TlsHandshakeFailed;
return .{
.reader = session.reader(),
.writer = session.writer(),
.negotiated_protocol = .http_1_1,
};
}
fn deinit(session: *Session) void {
session.conn.close();
}
comptime {
http.tls.validateBackend(@This());
}
};
pub fn main() !void {
const addr = std.net.Address.initIp4(.{ 0, 0, 0, 0 }, 8443);
var tcp = try std.net.Address.listen(addr, .{ .reuse_address = true });
defer tcp.deinit();
while (true) {
const accepted = try tcp.accept();
_ = try std.Thread.spawn(.{}, handleTlsConn, .{accepted.stream});
}
}
fn handleTlsConn(stream: std.net.Stream) void {
defer stream.close();
var read_buf: [8192]u8 = undefined;
var write_buf: [8192]u8 = undefined;
var net_reader = std.net.Stream.Reader.init(stream, &read_buf);
var net_writer = std.net.Stream.Writer.init(stream, &write_buf);
var session: TlsBackend.Session = undefined;
const result = TlsBackend.initServer(&session, net_reader.interface(), &net_writer.interface, .{
.cert_key = .{ .cert_pem = @embedFile("cert.pem"), .key_pem = @embedFile("key.pem") },
}) catch return;
defer TlsBackend.deinit(&session);
var server = http.Server.init(result.reader, result.writer, .{});
var head_buf: [4096]u8 = undefined;
var header_buf: [32]http.Header = undefined;
while (true) {
const req = server.recvRequest(&head_buf) catch return;
server.sendResponse(&head_buf, &header_buf, .ok, null, req,
&.{.{ .name = "Content-Type", .value = "text/plain" }},
"hello over TLS", .{}) catch return;
if (server.shouldClose()) return;
}
}libhttp's parsing utilities work standalone — you can use them inside any HTTP framework for header inspection, content negotiation, and caching logic without replacing the framework's transport.
zig fetch --save git+https://github.com/karlseguin/http.zig
zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize });
const libhttp_dep = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("httpz", httpz_dep.module("httpz"));
exe.root_module.addImport("http", libhttp_dep.module("libhttp"));const std = @import("std");
const httpz = @import("httpz");
const http = @import("http");
pub fn main() !void {
var server = try httpz.Server().init(std.heap.page_allocator, .{ .port = 8080 });
defer server.deinit();
server.router().get("/resource", handleResource);
server.router().get("/file", handleRangedFile);
try server.listen();
}
fn handleResource(_: *httpz.Request, resp: *httpz.Response) !void {
// Content negotiation via libhttp.
const accept = resp.req.header("accept") orelse "*/*";
const offered = [_]http.common.MediaType{
.{ .type_ = "application", .subtype = "json" },
.{ .type_ = "text", .subtype = "html" },
};
if (http.common.selectMediaType(accept, &offered)) |idx| {
if (idx == 0) {
resp.content_type = .JSON;
resp.body = "{\"status\":\"ok\"}";
} else {
resp.content_type = .HTML;
resp.body = "<h1>OK</h1>";
}
} else {
resp.status = 406;
}
// Cookie inspection.
if (resp.req.header("cookie")) |cookies| {
if (http.common.findCookie(cookies, "session_id")) |sid| {
std.log.info("session: {s}", .{sid});
}
}
// Cache-control parsing.
if (resp.req.header("cache-control")) |cc| {
if (http.common.getMaxAge(cc)) |age| {
std.log.info("client max-age: {d}", .{age});
}
}
}
fn handleRangedFile(req: *httpz.Request, resp: *httpz.Response) !void {
const file_size: u64 = 102400;
if (req.header("range")) |range_hdr| {
var it = http.common.RangeIterator.init(range_hdr) catch {
resp.status = 416;
return;
};
if (it.next()) |range| {
const resolved = http.common.resolveRange(range, file_size) orelse {
resp.status = 416;
return;
};
resp.status = 206;
var buf: [64]u8 = undefined;
const cr = std.fmt.bufPrint(&buf, "bytes {d}-{d}/{d}", .{
resolved.first, resolved.last, file_size,
}) catch return;
resp.header("Content-Range", cr);
// ... serve resolved.first..resolved.last bytes ...
}
}
}httpz handles HTTP/1.1 routing. For connections negotiated as HTTP/2 (via ALPN during TLS), route them to libhttp's H2 module instead.
const std = @import("std");
const Io = std.Io;
const http = @import("http");
fn handleConnection(stream: std.net.Stream) void {
defer stream.close();
var read_buf: [8192]u8 = undefined;
var write_buf: [8192]u8 = undefined;
var net_reader = std.net.Stream.Reader.init(stream, &read_buf);
var net_writer = std.net.Stream.Writer.init(stream, &write_buf);
// Perform TLS handshake with your backend.
var session: MyTlsBackend.Session = undefined;
const tls_result = MyTlsBackend.initServer(
&session, net_reader.interface(), &net_writer.interface, server_tls_config,
) catch return;
defer MyTlsBackend.deinit(&session);
switch (tls_result.negotiated_protocol) {
.h2 => handleH2(tls_result.reader, tls_result.writer),
.http_1_1, .unknown => handleH1(tls_result.reader, tls_result.writer),
.h3 => unreachable, // H3 uses QUIC, not TCP+TLS
}
}
fn handleH1(reader: *Io.Reader, writer: *Io.Writer) void {
// Use httpz for HTTP/1.1 routing, or libhttp's H1 server directly.
var server = http.Server.init(reader, writer, .{});
var head_buf: [4096]u8 = undefined;
var header_buf: [32]http.Header = undefined;
while (true) {
const req = server.recvRequest(&head_buf) catch return;
server.sendResponse(&head_buf, &header_buf, .ok, null, req,
&.{.{ .name = "Content-Type", .value = "text/plain" }},
"HTTP/1.1 response", .{}) catch return;
if (server.shouldClose()) return;
}
}
fn handleH2(reader: *Io.Reader, writer: *Io.Writer) void {
const h2 = http.h2;
var stream_slots: [64]h2.Stream = undefined;
var dec_entries: [128]h2.hpack.EntryMeta = undefined;
var dec_data: [8192]u8 = undefined;
var enc_entries: [128]h2.hpack.EntryMeta = undefined;
var enc_data: [8192]u8 = undefined;
var decoder = h2.hpack.Decoder.init(&dec_entries, &dec_data, 4096);
var encoder = h2.hpack.Encoder.init(&enc_entries, &enc_data, 4096);
var streams = h2.StreamTable.init(&stream_slots, true);
var server = http.Server.initH2(reader, writer, .{}, &streams, &decoder, &encoder);
var frame_buf: [16393]u8 = undefined;
server.handshake(&frame_buf) catch return;
var hdr_block_buf: [8192]u8 = undefined;
var headers_out: [64]http.Header = undefined;
var name_buf: [4096]u8 = undefined;
var value_buf: [8192]u8 = undefined;
while (true) {
const event = server.recvH2Frame(
&frame_buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf,
) catch return;
switch (event) {
.headers => |h| {
server.sendH2Headers(&frame_buf, h.stream_id, &.{
.{ .name = ":status", .value = "200" },
.{ .name = "content-type", .value = "text/plain" },
}, true) catch return;
},
.goaway => return,
else => {},
}
}
}