Files
README.md

zttp #

an HTTP client for zig 0.17, built on std.http.Client. it adds the things a program calling servers it does not control needs and std leaves to the caller: timeouts, a response size cap, a retry for stale pooled connections, a redirect policy that keeps authorization on the origin it was meant for, and validation of URLs and headers.

install #

requires zig 0.17+. on 0.16, use v0.1.x.

zig fetch --save https://tangled.org/zat.dev/zttp/archive/main

then in build.zig:

const zttp = b.dependency("zttp", .{}).module("zttp");
exe.root_module.addImport("zttp", zttp);

use #

const zttp = @import("zttp");

var client = zttp.Client.initWithUserAgent(io, allocator, "myapp/1.0 (+https://example.com)");
defer client.deinit();

var res = try client.fetch(.{
    .url = "https://example.com/api",
    .capture_headers = &.{"content-type"},
});
defer res.deinit(allocator);

// res.status, res.body, res.headers.get("content-type"), res.rate_limit

fetch buffers the body in memory. to write it somewhere else, open returns once the response head has arrived and stream drains the body into any std.Io.Writer:

const response = try client.open(.{ .url = url });
defer response.deinit();
_ = try response.stream(&file_writer.interface, .{});

limits are fields on Client with defaults, and FetchOptions overrides them for one request.

what it adds to std.http.Client #

  • timeouts — one deadline covers connect, TLS, sending the request and waiting for the response head. the body is cut off if no bytes arrive for a while or if it arrives too slowly to finish. a request can also carry one overall deadline.
  • a size cap — a buffered response larger than the cap is an error.
  • retry of stale connections — when a pooled keep-alive connection turns out to be closed or reset before any response arrives, a request with an idempotent method and no body is sent again on a new connection. nothing else is retried.
  • redirects — zttp follows 3xx itself. authorization is dropped when a hop changes scheme, host or port, loops are an error, the number of hops is capped, and https to http is refused unless allowed.
  • input validation — a URL with a control character, or a header with CR or LF, is an error before anything is sent.
  • rate-limit headers — RateLimit-* and Retry-After are parsed into res.rate_limit, and delaySeconds turns them into one wait.
  • named errors — zttp.Error is the set of errors the client adds to std's.

the timeouts need an Io that can run tasks concurrently. on one that cannot, the request runs without them and zttp logs a warning.

docs/client.md explains each of these and why it is there.

develop #

zig build test    # loopback servers: stalls, silent peers, closed pooled connections, redirects
zig build smoke   # real servers over the network: TLS, gzip, redirects, a peer that never answers

license #

MIT


changelog

TLS trust #

Version 0.2.0-alpha.4 removes the four roots and eight compromised intermediate public keys from the October 2026 eMudhra incident from loaded CA bundles. This includes caller-supplied bundles. Chains anchored only at those roots fail, including otherwise valid certificates. Keep the OS trust store updated as well; Zig's macOS keychain loader does not apply native revocation policy.

zig build test verifies the original certificate fixtures offline. zig build trust-probe checks both public bad-chain hosts, the AT Protocol identity host, and an ordinary HTTPS control through the actual client.

The optional local macOS toolchain patch also protects Zig 0.17.0's own package fetcher. It defaults to dry-run, requires an exact source checksum, and retains a reversible backup.