Skip to content

Client API ​

The httpx.zig client provides a high-level HTTP client for making requests over HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3. Plain HTTPS/1.1 uses std.crypto.tls for TLS 1.2/1.3. Explicit HTTP/2 requests and mutual TLS use the native TLS 1.3 client built on std.crypto primitives (AES-GCM, ChaCha20-Poly1305, X25519, HKDF, SHA-256/384/512), including ALPN negotiation. HTTP/3 runs over QUIC with its own TLS 1.3 handshake.

Protocol Support ​

ProtocolStatusTransportNotes
HTTP/1.0✅ FullTCPLegacy support
HTTP/1.1✅ FullTCP/TLSDefault protocol
HTTP/2✅ Client Runtime + PrimitivesTCP/TLSHigh-level client request execution path plus full framing/HPACK/stream primitives
HTTP/3✅ Reliable pathsQUIC/UDPHigh-level client runtime over QUIC/TLS 1.3 with ALPN h3, QPACK dynamic tables, loss recovery, NewReno congestion control, 0-RTT resumption, Retry/version negotiation, key updates, and pooled connections for sequential reuse

HTTP/3 runtime mode performs a QUIC TLS 1.3 handshake and verifies the server certificate. Loopback integration is covered, including dynamic QPACK and response trailers. QUIC loss recovery, NewReno congestion control, Retry/version negotiation, key updates, and connection pooling for sequential requests are active; the remaining transport limits are HelloRetryRequest, connection migration, and concurrent multiplexing over a shared high-level HTTP/3 connection.

The protocol module provides HTTP/2 and HTTP/3 building blocks (HPACK/QPACK, framing, and transport primitives). See Protocol API for details.

Proxy Modes ​

httpx.zig supports HTTP CONNECT and the SOCKS4/SOCKS5 proxy families:

KindBehaviorDNS resolution
httpHTTP CONNECT tunnelProxy resolves the target host
socks5SOCKS5Client resolves the target host
socks5hSOCKS5 with remote host resolutionProxy resolves the hostname
socks4SOCKS4, IPv4 destinations onlyClient resolves the target host
socks4aSOCKS4a with hostnames forwarded unresolvedProxy resolves the hostname

Use socks5h when you want to avoid local DNS lookups or when the proxy has access to names that are not visible on the client network. https:// proxy URLs are rejected, and HTTP/3 requests cannot use a proxy (Http3ProxyUnsupported).

Client ​

The Client struct is the main entry point for making requests. It manages connection pooling, DNS caching, and retries.

Initialization ​

zig
const std = @import("std");
const httpx = @import("httpx");

const io = std.Io.Threaded.global_single_threaded.io();

// Initialize with default configuration (returns Client by value)
var client = httpx.Client.init(allocator, io, .{});
defer client.deinit();

// Initialize with custom configuration
var client = httpx.Client.init(allocator, io, .{
    .httpVersion = .auto, // or .http10, .http11, .http2, .http3
    .timeoutMs = 10_000,
    .followRedirects = true,
});
defer client.deinit();

// Requests use the initialized client and its allocator/I/O context.
var response = try client.get("https://api.example.com/users", .{});
defer response.deinit();

var post_res = try client.post("https://api.example.com/users", .{ .json = .{ .name = "Alice", .role = "developer" },
});
defer post_res.deinit();

The client configuration is an idiomatic Zig struct passed to Client.init; omitted fields use standard defaults. It mirrors Config in src/client/httpClient.zig:

zig
const io = std.Io.Threaded.global_single_threaded.io();
var client = httpx.Client.init(allocator, io, .{
    .timeoutMs = 10_000,
    .followRedirects = true,
    .maxRedirects = 5,
    .maxRetries = 3,
    .retryDelayMs = 500,
    .retryStatusCodes = &.{ 502, 503, 504 },
    .httpVersion = .http2,
    .pool = .{ .maxConnections = 64, .maxPerHost = 16 },
    .proxy = null,
});
defer client.deinit();

// Explicit client environment configuration using httpx.env utils:
// Applications selectively read terminal/process variables or .env files and pass them into .{}
var client_with_env = httpx.Client.init(allocator, io, .{
    .proxy = httpx.env.get("HTTPS_PROXY") orelse httpx.env.get("HTTP_PROXY"),
    .timeoutMs = httpx.env.getInt(u64, "REQUEST_TIMEOUT_MS") orelse 10_000,
    .maxRetries = httpx.env.getInt(u32, "MAX_RETRIES") orelse 3,
});
defer client_with_env.deinit();

// Explicit .env file loading:
var envStore = try httpx.env.loadFile(allocator, ".env");
defer envStore.deinit();
const apiKey = envStore.get("MY_API_KEY");

Configuration Fields ​

FieldTypeDefaultDescription
timeoutMs?u64nullDefault request timeout in milliseconds.
followRedirectsbooltrueWhether to automatically follow redirects.
maxRedirectsu810Maximum redirects to follow per request.
maxRetriesu320Retry attempts for failed requests (0 = disabled).
retryDelayMsu641000Base delay between retries in milliseconds.
retryStatusCodes[]const u16&.{ 502, 503, 504 }Status codes that trigger a retry.
retryUnsafeMethodsboolfalseAlso retry POST, PATCH, and CONNECT; leave false unless the server deduplicates.
earlyDataEarlyDataOptions{}Default 0-RTT early-data settings.
httpVersion?HttpVersionnullPreferred HTTP version (.auto, .http10, .http11, .http2, .http3).
http10booltrueFast toggle to enable HTTP/1.0 protocol.
http11booltrueFast toggle to enable HTTP/1.1 protocol.
http2boolfalseFast toggle to use HTTP/2 as default protocol.
http3boolfalseFast toggle to use HTTP/3 as default protocol.
poolPoolConfig{}Connection pool limits (maxConnections, maxPerHost, idleTimeoutMs, maxParkedMs).
dnsCacheDnsCacheOptions{}DNS cache settings (enable, ttlMs, negativeTtlMs, maxEntries).
tls?TlsOptionsnullDefault TLS overrides for https://; null uses system trust verification.
proxy?[]const u8nullDefault proxy URL (http://, socks5://, socks5h://, socks4://, socks4a://).
maxResponseSize?usizenullDefault maximum response body size.
allowLfLineEndingsboolfalseAccept bare LF line endings from non-compliant peers.
eventCallback?ClientEventCallbacknullApplication callback for client events (silent by default).

If you do not set a field, the implicit default value is used. .{} is the safe default.

HTTPS requests verify the server certificate against system trust roots by default, including when tls is omitted or null. TLS truncation is rejected unless explicitly enabled with TlsOptions.allowTruncation; set verify = .none only for controlled testing or development.

Timeouts ​

The client uses a single uniform timeout model. The client configuration's timeoutMs sets the default deadline applied to requests; per-request timeoutMs overrides it for that request.

zig
const io = std.Io.Threaded.global_single_threaded.io();
var client = httpx.Client.init(allocator, io, .{
    .timeoutMs = 10_000,
});
defer client.deinit();

var res = try client.get("https://example.com/slow", .{ .timeoutMs = 2_000 });
defer res.deinit();

Client Initialization ​

FunctionDescription
Client.init(allocator, io, config)Initialize client with allocator, io, and configuration (or .{} for defaults).

Primary Unified API: fetch ​

client.fetch(url, options) is the primary high-level HTTP client operation. It supports all HTTP methods, strongly typed JSON serialization, custom headers, query parameters, timeouts, and body streaming in a single call:

zig
// Simple GET
var res = try client.fetch("https://api.example.com/data", .{});
defer res.deinit();

// POST with strongly typed Zig struct (automatically serialized via std.json)
const CreateUser = struct { name: []const u8, email: []const u8 };
const User = struct { id: u64, name: []const u8, email: []const u8 };

var res2 = try client.fetch("https://api.example.com/users", .{
    .method = .POST,
    .headers = .{ .Authorization = "Bearer secret_token" },
    .json = CreateUser{ .name = "Fiaz", .email = "fiaz@example.com" },
});
defer res2.deinit();

// Strongly typed response deserialization:
const user = try res2.json(User);
std.debug.print("User created: id={d} name={s}\n", .{ user.id, user.name });

// Managed lifecycle deserialization with explicit allocator:
const parsed = try res2.jsonAlloc(User, allocator);
defer parsed.deinit();

Response Methods ​

MethodDescription
json(comptime T: type) !TParses response body as JSON into type T using the response's allocator.
jsonAlloc(comptime T: type, allocator: Allocator) !std.json.Parsed(T)Parses response body as JSON with explicit allocator and managed lifecycle.
bytes() []const u8Returns response body as a raw byte slice.
text() []const u8Returns response body as a UTF-8 text string.
writeTo(writer: anytype) !voidWrites the buffered body bytes to any writer (file, stdout, buffer).
header(name: []const u8) ?[]const u8Case-insensitive header lookup.
status u16Returns HTTP status code integer.
isSuccess() boolReturns true if status code is in 200..299 range.
isRedirect() boolReturns true if status code is in 300..399 range.
isInformational() boolReturns true if status code is in 100..199 range.

Methods ​

All methods are URL-first (url, then options):

MethodDescription
fetch(url, options)Primary unified HTTP request operation
get(url, options)HTTP GET request
post(url, options)HTTP POST request (accepts .body, .json, .form)
postJson(url, payload, options)HTTP POST with typed struct or serialized JSON payload
put(url, options)HTTP PUT request
delete(url, options)HTTP DELETE request
patch(url, options)HTTP PATCH request
head(url, options)HTTP HEAD request
trace(url, options)HTTP TRACE request
connect(url, options)HTTP CONNECT request
options(url, options)HTTP OPTIONS request
request(url, options)Generic request (method set via options.method)
requestStream(url, options)Streaming request returning ResponseStream (incremental on HTTP/1.1)
requestBuilder(method, url)Start fluent RequestBuilder for chained request construction
builder(method, url)Alias for requestBuilder
getBuilder(url)Start GET RequestBuilder
postBuilder(url)Start POST RequestBuilder
getAll(urls)Concurrent GETs; deinit each response, free the slice
requestAll(reqs)Concurrent requests; same ownership
download(url, options)Streaming file download with resume/verify (destination via options.path)
downloadBatch(tasks, options)Concurrent downloads
lookupFileInfo(url, options)Remote metadata without body download
updateFile(url, options)Atomic file update with rollback (target via options.path)
graphql(url, query, variables, options)GraphQL request
fetchDocument/fetchHtml/fetchXml/fetchFeed/fetchRobots/fetchSitemap(url, options)Fetch + parse documents
resolve(host, options) / resolveUrl(url, options)DNS resolution (port via options.port)
close()Purge the connection pool
reset()Close + clear DNS cache

The standalone httpx.client.cookieJar keeps cookies across requests (domain-filtered per RFC 6265). Per-request cookies use the cookie option.

MethodDescription
cookieJar.init(allocator, io) / deinit()Lifecycle
setFromHeader(setCookie, host)Store a Set-Cookie value
cookieHeader(host, path, secure, buf)Render the Cookie header (secure gates Secure cookies)
purgeExpired()Drop expired cookies

Quick Examples ​

zig
const std = @import("std");
const httpx = @import("httpx");

const io = std.Io.Threaded.global_single_threaded.io();
var client = httpx.Client.init(allocator, io, .{});
defer client.deinit();

// Simple GET
var response = try client.get("https://api.example.com/users", .{});
defer response.deinit();
std.debug.print("Status: {d}\n", .{response.status});
std.debug.print("Body: {s}\n", .{response.text()});

// POST with JSON
var json_response = try client.post("https://api.example.com/users", .{
    .json = "{\"name\": \"John\", \"email\": \"john@example.com\"}",
});
defer json_response.deinit();

// POST with typed struct
const User = struct { name: []const u8, role: []const u8 };
var typed_res = try client.postJson("https://api.example.com/users", User{
    .name = "John",
    .role = "engineer",
}, .{});
defer typed_res.deinit();

// Custom headers
var auth_response = try client.get("https://api.example.com/protected", .{
    .headers = &.{
        .{ .name = "Authorization", .value = "Bearer token123" },
        .{ .name = "X-Custom-Header", .value = "value" },
    },
});
defer auth_response.deinit();

// Built-in auth helpers
var bearer_response = try client.get("https://api.example.com/protected", .{
    .bearerAuth = "token123",
});
defer bearer_response.deinit();

// With timeout
var timeout_response = try client.get("https://slow-api.com/data", .{
    .timeoutMs = 30000, // 30 seconds
});
defer timeout_response.deinit();

Client Usage Recipes ​

For complete copy/paste demos, see these example pages:

Request Options (RequestOptions) ​

Per-request overrides for configuration. The client accepts these fields directly (plus duck-typed extras such as multipart and graphql handled via anytype opts). Unset optional fields fall back to client-level defaults.

FieldTypeDefaultDescription
method?MethodnullExplicit method for generic request/fetch (wrappers supply their own).
headers[]const Header&.{}Additional headers ({ .name, .value } pairs).
query[]const Header&.{}Percent-encoded query params appended to the URL.
body?[]const u8nullRaw request body.
json?[]const u8nullJSON string body (sets Content-Type).
form?[]const u8nullPre-encoded application/x-www-form-urlencoded body.
text?[]const u8nullPlain-text body.
contentType?[]const u8nullExplicit Content-Type override.
basicAuth?[]const u8null"user:pass" encoded as Authorization: Basic ....
bearerAuth?[]const u8nullSets Authorization: Bearer <token>.
cookie?[]const u8nullCookie header value for this request.
timeoutMs?u64nullRequest-specific timeout override.
maxResponseSize?usizenullRequest-specific max response body size.
followRedirects?boolnullOverride client redirect setting.
maxRedirects?u8nullOverride client max-redirect limit.
allowLfLineEndingsboolfalseAccept bare LF line endings in the response.
httpVersion?HttpVersionnullForce a request over a specific protocol runtime.
http10 / http11 / http2 / http3?boolnullPer-request protocol toggles.
tls?TlsOptionsnullPer-request TLS overrides; null inherits the client default or system trust.
proxy?[]const u8nullPer-request proxy URL override.

Request options are configured directly using struct literals:

zig
var res = try client.get("/users", .{
    .headers = &.{.{ .name = "Accept", .value = "application/json" }},
    .query = &.{.{ .name = "page", .value = "1" }},
    .timeoutMs = 10_000,
    .httpVersion = .http2,
    .followRedirects = true,
});
defer res.deinit();

Multipart File Uploads ​

Use the multipart option to send multipart/form-data bodies. The client assembles the body and sets the Content-Type header (see examples/multipart.zig).

zig
var resp = try client.post("https://example.com/upload", .{
    .multipart = .{
        .name = "upload",
        .filename = "data.bin",
        .contentType = "application/octet-stream",
        .data = chunk_slice,
    },
});
defer resp.deinit();

Windows — buffer limit (issue #26)

On Windows the Winsock kernel send buffer is typically 8–64 KB. When sending large multipart data as a single body, winsock.send() may stall.

httpx.zig 0.1.8+ automatically caps each send call to 64 KB, so most uploads now work without application changes. For extra safety — especially for payloads larger than a few hundred KB — build the body with httpx.multipart.encoder.Multipart (or post a single part inline with the .multipart request option) and issue one request per slice. See the Multipart Guide for a complete resumable upload example.

Response ​

The Response struct contains the server's response (src/client/requestBuilder.zig).

zig
pub const Response = struct {
    allocator: Allocator,
    status: u16,
    version: HttpVersion,
    headers: []Header,
    body: []u8,

    pub fn deinit(self: *Response) void
    pub fn header(self: *const Response, name: []const u8) ?[]const u8
    pub fn text(self: *const Response) []const u8
    pub fn bytes(self: *const Response) []const u8
    pub fn writeTo(self: *const Response, writer: anytype) !void
    pub fn contentType(self: *const Response) []const u8
    pub fn json(self: *const Response, comptime T: type) !T
    pub fn jsonAlloc(self: *const Response, comptime T: type, allocator: Allocator) !std.json.Parsed(T)
    pub fn isInformational(self: *const Response) bool
    pub fn isSuccess(self: *const Response) bool
    pub fn isRedirect(self: *const Response) bool
};

Response Methods ​

MethodDescription
deinit()Free response resources
header(name)Get header value by name (case-insensitive)
text() / bytes()Get response body bytes
writeTo(writer)Write the buffered body bytes to any writer
contentType()Response Content-Type value
json(T)Parse body as JSON into T (ignores unknown fields)
jsonAlloc(T, allocator)Parse body as JSON, returning std.json.Parsed(T)
isInformational()Status 100-199
isSuccess()Status 200-299
isRedirect()Status 300-399

Middleware ​

HTTPX has no client interceptor registry. Cross-cutting request/response behavior belongs in server middleware (httpx.middleware.*) or in small wrappers around the URL-first client calls. The examples/interceptorExample.zig demo exercises a plain server route plus a client GET against it.

Error Handling ​

zig
const response = client.get("https://example.com", .{}) catch |err| switch (err) {
    error.ConnectionRefused => {
        std.debug.print("Server not available\n", .{});
        return;
    },
    error.Timeout => {
        std.debug.print("Request timed out\n", .{});
        return;
    },
    error.TlsError => {
        std.debug.print("TLS handshake failed\n", .{});
        return;
    },
    else => return err,
};

Request Methods ​

Use request methods on an initialized client:

zig
var a = try client.get("https://example.com", .{});
defer a.deinit();

var b = try client.post("https://example.com/items", .{ .json = "{\"name\":\"demo\"}",
});
defer b.deinit();

var c = try client.delete("https://example.com/items/42", .{});
defer c.deinit();

var d = try client.put("https://example.com/items", .{ .json = "{\"name\":\"updated\"}",
});
defer d.deinit();

var e = try client.patch("https://example.com/items", .{ .json = "{\"name\":\"patched\"}",
});
defer e.deinit();

var f = try client.head("https://example.com/items", .{});
defer f.deinit();

var g = try client.options("https://example.com/items", .{});
defer g.deinit();

var h = try client.request("https://example.com/health", .{
    .method = .GET,
});
defer h.deinit();

Client request methods (all URL-first):

FunctionDescription
client.get(url, opts)HTTP GET
client.post(url, opts)HTTP POST
client.put(url, opts)HTTP PUT
client.patch(url, opts)HTTP PATCH
client.delete(url, opts)HTTP DELETE
client.head(url, opts)HTTP HEAD
client.options(url, opts)HTTP OPTIONS
client.trace(url, opts)HTTP TRACE
client.connect(url, opts)HTTP CONNECT
client.fetch(url, opts)Unified fetch (any method via .method)
client.request(url, opts)General HTTP request (any method via .method)
client.getAll(&urls)Concurrent GET requests
client.requestAll(&requests)Concurrent custom requests

See Also ​

Released under the MIT License.