An HTTP client is code that sends a request to a web server and reads the response.
When you open a web page, your browser acts as an HTTP client. When a command-line tool downloads a file, it may act as an HTTP client. When a program calls a web API, it usually uses an HTTP client.
HTTP is built around requests and responses.
A request asks for something:
GET / HTTP/1.1
Host: example.comA response sends back a status, headers, and a body:
HTTP/1.1 200 OK
Content-Type: text/html
<html>...</html>URLs
An HTTP request usually starts from a URL.
https://example.com/index.htmlThis has several parts.
httpsis the scheme. It tells the client to use HTTPS.
example.comis the host.
/index.htmlis the path.
A URL may also contain a port, query string, username, password, or fragment. For beginners, scheme, host, and path are enough.
GET Requests
The most common HTTP method is GET.
A GET request asks the server for a resource.
Examples:
GET https://example.com/
GET https://api.example.com/users
GET https://example.com/file.txtA Zig HTTP client call usually follows this shape:
const uri = try std.Uri.parse("https://example.com/");Then the client uses that URI to make a request.
The exact std.http.Client API can change across Zig versions, so check your local documentation with:
zig stdThe important ideas are stable:
create a client
parse a URL
send a request
read the response
close or deinitialize resources
A Conceptual HTTP GET
A simple HTTP GET program has this shape:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var client = std.http.Client{
.allocator = allocator,
};
defer client.deinit();
const uri = try std.Uri.parse("https://example.com/");
// Send GET request.
// Read response body.
// Print response body.
}This shows the resource pattern.
The allocator belongs to the general purpose allocator.
The HTTP client uses the allocator.
The client is deinitialized with defer.
The URI parse can fail because the URL may be invalid.
Request and Response
HTTP has two sides.
The request contains:
method
URL
headers
optional body
The response contains:
status code
headers
optional body
A basic GET request usually has no body.
A POST request often has a body.
For example, a JSON API request may send:
{"name":"Alice"}and receive:
{"ok":true}Status Codes
The status code tells you what happened.
| Code | Meaning |
|---|---|
200 | OK |
201 | Created |
204 | No content |
301 | Moved permanently |
302 | Found or redirected |
400 | Bad request |
401 | Unauthorized |
403 | Forbidden |
404 | Not found |
500 | Server error |
Do not assume a request succeeded only because the network call returned.
The request may complete successfully at the transport level but still return:
404 Not Foundor:
500 Internal Server ErrorYour program should inspect the status code.
Headers
Headers are key-value metadata.
Example request headers:
User-Agent: my-tool/1.0
Accept: application/json
Authorization: Bearer TOKENExample response headers:
Content-Type: application/json
Content-Length: 123
Cache-Control: max-age=60Headers are still text. Their meaning depends on the HTTP protocol and the server.
Response Body
The response body is the main data returned by the server.
It might be:
HTML
JSON
plain text
an image
a compressed file
binary data
Your program decides how to interpret the body.
For an API, you may parse the body as JSON.
For a downloader, you may write the body to a file.
For a health check, you may only inspect the status code.
Reading the Whole Body
For small responses, reading the whole body into memory is convenient.
Conceptually:
const body = try response.reader().readAllAlloc(allocator, max_size);
defer allocator.free(body);The max_size matters.
Never read an untrusted HTTP response into memory without a limit.
A server could send a huge response and exhaust your memory.
The pattern should include a maximum:
const max_body_size = 1024 * 1024;This means 1 MiB.
Streaming the Body
For large responses, stream the body.
The shape is:
read chunk from response
write chunk to file
repeat until doneThis avoids storing the whole response in memory.
Streaming is the right approach for:
large downloads
logs
media
archives
database exports
unknown-size responses
The same pattern appeared in file I/O and compression. Systems programming repeats this idea often.
Timeouts
HTTP clients should use timeouts.
Without timeouts, a program can hang for a long time if the server stops responding.
There are different kinds of timeouts:
connection timeout
read timeout
total request timeout
idle timeout
The exact API depends on the HTTP client implementation, but the design rule is simple:
Network programs should not wait forever by accident.
HTTPS
Most modern HTTP traffic uses HTTPS.
HTTPS means HTTP over TLS.
TLS encrypts the connection and verifies the server identity.
When using HTTPS, the client must support certificates and TLS verification.
Do not disable certificate verification in production code. That defeats a major part of HTTPS security.
Sending JSON
Many APIs accept JSON.
A request may need:
method POST
header Content-Type: application/json
JSON body
Conceptually:
POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"name":"Alice"}In Zig, you can format or stringify the JSON body, then send it as bytes.
The important rule is:
JSON is text, but HTTP sends bytes.
So the request body is a []const u8.
Parsing JSON Responses
If the server returns JSON, parse it after reading the body.
Conceptual shape:
const parsed = try std.json.parseFromSlice(
Response,
allocator,
body,
.{},
);
defer parsed.deinit();
const value = parsed.value;This combines HTTP and JSON:
HTTP retrieves bytes.
JSON parsing turns bytes into typed data.
Keep those layers separate in your thinking.
User-Agent
Some servers expect a User-Agent header.
A good command-line tool should identify itself.
Example:
User-Agent: my-zig-tool/0.1This helps server operators understand traffic and debug problems.
Do not pretend to be a browser unless you have a specific compatibility reason.
Redirects
A server may respond with a redirect.
For example:
301 Moved Permanently
Location: https://www.example.com/Some HTTP clients follow redirects automatically. Others require you to handle them.
For tools, make this behavior deliberate.
A downloader may follow redirects.
A security-sensitive client may want stricter rules.
Errors
HTTP code can fail at many layers.
The URL may be invalid.
DNS lookup may fail.
The TCP connection may fail.
TLS verification may fail.
The server may close the connection.
The response may be too large.
The status code may indicate failure.
The JSON body may be invalid.
A good program distinguishes these cases when it matters.
At minimum, do not collapse every error into “request failed” unless the caller truly does not need detail.
A Small API Client Shape
Here is the shape of a small typed API client:
const std = @import("std");
const User = struct {
id: u64,
name: []const u8,
};
fn fetchUser(
allocator: std.mem.Allocator,
id: u64,
) !std.json.Parsed(User) {
var client = std.http.Client{
.allocator = allocator,
};
defer client.deinit();
var url_buffer: [256]u8 = undefined;
const url = try std.fmt.bufPrint(
&url_buffer,
"https://api.example.com/users/{}",
.{id},
);
const uri = try std.Uri.parse(url);
// Send HTTP GET.
// Read body with a maximum size.
// Parse body as User.
// Return parsed JSON object.
}The function signature is important:
fn fetchUser(
allocator: std.mem.Allocator,
id: u64,
) !std.json.Parsed(User)It says:
the function may allocate
the function may fail
the result owns parsed memory
the caller must call deinit
Avoid Mixing Too Much in One Function
A clean HTTP program usually has layers.
One function builds the URL.
One function sends the request.
One function checks the status.
One function parses JSON.
One function handles user-facing errors.
This keeps code testable.
For example:
fn buildUserUrl(buffer: []u8, id: u64) ![]u8
fn fetchBytes(allocator: std.mem.Allocator, url: []const u8) ![]u8
fn parseUser(allocator: std.mem.Allocator, body: []const u8) !std.json.Parsed(User)Small functions make network code easier to reason about.
Common Mistakes
Do not read unlimited response bodies into memory.
Do not ignore status codes.
Do not treat HTTP success and API success as the same thing.
Do not disable TLS verification casually.
Do not log secret headers such as Authorization.
Do not assume the response body is valid JSON.
Do not forget to deinitialize the HTTP client or free response buffers.
Do not build URLs with unchecked user input.
The Core Pattern
For a basic HTTP client:
var client = std.http.Client{
.allocator = allocator,
};
defer client.deinit();
const uri = try std.Uri.parse("https://example.com/");
// send request
// read response
// inspect status
// use bodyFor JSON APIs:
const body = try read_response_body_with_limit;
const parsed = try std.json.parseFromSlice(
ResponseType,
allocator,
body,
.{},
);
defer parsed.deinit();For large downloads:
open output file
read response chunks
write chunks to file
close output fileWhat You Should Remember
An HTTP client sends requests and reads responses.
A URL identifies the target resource.
A response has a status code, headers, and body.
A successful network operation can still return an HTTP error status.
Read small bodies into memory only with a size limit.
Stream large bodies.
Use HTTPS correctly.
Treat response bodies as external input.
HTTP client code is mostly resource management, error handling, byte handling, and protocol discipline.