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. HTTPS is supported via a fully custom TLS 1.2/1.3 implementation built on std.crypto primitives (AES-GCM, ChaCha20-Poly1305, X25519, HKDF, SHA-256/384/512) with ALPN negotiation for HTTP/2 and HTTP/3.

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✅ Client Runtime + PrimitivesQUIC/UDPHigh-level client runtime over UDP + QUIC/HTTP3/QPACK primitives (suitable for local/integration endpoints)

HTTP/3 runtime mode is available in the high-level client and uses QUIC packet/stream framing primitives directly. Interoperability with endpoints that require full TLS-in-QUIC handshake negotiation may vary depending on deployment expectations.

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 two client proxy modes:

KindBehaviorDNS resolution
httpStandard forward proxy or HTTPS CONNECT tunnelClient resolves the target host unless the proxy protocol performs the tunnel itself
socks5hSOCKS5 proxy with remote host resolutionProxy resolves the hostname and connects on behalf of the client

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.

Client ​

The Client struct is the main entry point for making requests. It manages connection pooling, cookies, and interceptors.

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();

// Out-of-the-box convenience (zero initialization needed):
var response = try httpx.get("https://api.example.com/users");
defer response.deinit();

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

ClientConfig is an idiomatic Zig struct where omitted fields use standard defaults. It matches httpx.ClientConfig in src/client/client.zig exactly:

zig
const io = std.Io.Threaded.global_single_threaded.io();
const cfg: httpx.ClientConfig = .{
    .timeoutMs = 10_000,
    .followRedirects = true,
    .maxRedirects = 5,
    .maxRetries = 3,
    .retryDelayMs = 500,
    .retryStatusCodes = &.{ 502, 503, 504 },
    .httpVersion = .http2,
    .pool = .{ .maxConnections = 64, .maxPerHost = 16 },
    .proxy = null,
    .cookies = true,
};

var client = httpx.Client.init(allocator, io, cfg);
defer client.deinit();

Configuration (ClientConfig) ​

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.
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.
cookiesbooltrueEnable cookie jar handling.
poolPoolConfig{}Connection pool limits (maxConnections, maxPerHost, idleTimeoutMs, maxParkedMs).
dnsCacheDnsCacheOptions{}DNS cache settings (enable, ttlMs, negativeTtlMs, maxEntries).
tls?TlsOptionsnullDefault TLS options for https:// requests.
proxy?[]const u8nullDefault proxy URL (http://, socks5://, socks5h://).
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.

Timeouts ​

The client uses a single uniform timeout model. ClientConfig.timeoutMs sets the default deadline applied to requests; per-request RequestOptions.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();

const 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) !voidStreams or writes response body directly 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
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)
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.CookieJar keeps cookies across requests (domain-filtered per RFC 6265). Per-request cookies use the cookie option.

MethodDescription
CookieJar.init(allocator) / 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
const 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.body});

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

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

// Built-in auth helpers
const bearer_response = try client.get("https://api.example.com/protected", .{ .headers = &.{.{ "Authorization", "Bearer token123" }, .{ "Accept", "application/json" }},
});
defer bearer_response.deinit();

// With timeout
const 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 options for https://.
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/request.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)Stream 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,
};

Convenience Functions ​

The root module exposes zero-config convenience helpers for simple requests:

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

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

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

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

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

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

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

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

Top-level request functions (all URL-first):

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

See Also ​

Released under the MIT License.