# 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. ```sh zig fetch --save https://tangled.org/zat.dev/zttp/archive/main ``` then in `build.zig`: ```zig const zttp = b.dependency("zttp", .{}).module("zttp"); exe.root_module.addImport("zttp", zttp); ``` ## use ```zig 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`: ```zig 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](docs/client.md) explains each of these and why it is there. ## develop ```sh 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](LICENSE) --- [changelog](CHANGELOG.md) ## TLS trust Version 0.2.0-alpha.4 removes the four roots and eight compromised intermediate public keys from the [October 2026 eMudhra incident](https://emudhra.we-have-a-problem.com/) 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](scripts/MACOS_TRUST.md) 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. ### Deadline and cancellation contract Head and total deadlines use absolute monotonic time across redirects and connection retries. A total deadline includes time between `open` and `stream`; `deadline_remaining_ns` is an informational snapshot at head arrival. Idle and rate checks use measured elapsed time. Configured guards require `Io.concurrent` and return `ConcurrencyUnavailable` when it cannot be provided. Completion events wake deadline watchers immediately; stall/rate sampling uses a separate timer. Cancellation joins the operation before returning, and incomplete transfers retire their connections. Dropping an unread response closes its connection without draining the body. An absent response or a timeout does not establish whether the origin executed the request; automatic retries remain restricted to payload-free GET, HEAD and OPTIONS. Synchronous TLS cryptography and caller sinks remain cooperative CPU work; a deadline cannot forcibly preempt them. ### HTTP qualification `just conformance-tools` prepares pinned tools. `just conformance` and `just conformance --optimize ReleaseSafe` run the isolated real-socket gate. See [coverage and provenance](tests/conformance/README.md) and the [audit findings](docs/audits/2026-11-07-io-conformance.md). ### Shutdown `Client.shutdown(timeout_ns)` stops new requests, waits for owned requests and responses, and closes idle TLS connections with `close_notify`. One absolute budget covers quiescence and all idle connections. New requests return `ClientClosed`; existing responses remain usable while shutdown waits. If that wait times out or is canceled, release/join those users before retrying shutdown. `deinit()` requires all client users and responses to be finished. It attempts shutdown using `shutdown_deadline_ns` (250 ms by default), aborts on failure, and frees the HTTP client. Call `shutdown` explicitly to inspect its outcome; repeated shutdown after the transport is closed is a no-op. Zero budget forces abort when TLS writes would be needed. Concurrency exhaustion is reported without falling back to an unbounded write. Successful connection retirement and pool eviction use the same bounded alert write. Abandoned or failed transfers abort immediately. The one-call `fetch` checks its total deadline after retirement cleanup too. No shutdown path waits for a peer's TLS alert. The Zig 0.17 pool adapter is isolated in `src/connection_pool.zig` and must be requalified on toolchain changes.