Making Requests
Dusty has an HTTP client too. Create one http.Client and use it for
the whole program:
The client keeps idle connections in a pool and reuses them for later requests to the same host. Tasks running at the same time can share it.
Fetching a URL
client.fetch sends a request and returns the response:
fn getPage(client: *http.Client) !void {
var response = try client.fetch("https://ziglang.org/", .{});
defer response.deinit();
const body = try response.body() orelse "";
std.debug.print("{d}, {d} bytes\n", .{ @intFromEnum(response.status()), body.len });
}
fetch connects, sends the request, follows redirects and reads the whole body before it returns.
response.body() gives you that body, or null if it was empty. Call response.deinit() when you're
done. It puts the connection back in the pool. Anything you got from the response is only valid
until then.
The response also has status(), headers(), contentType() and contentLength().
Method, Headers and Body
The second argument to fetch sets the method, the headers and the body:
fn createUser(client: *http.Client, gpa: std.mem.Allocator) !void {
var headers = try http.Headers.init(gpa, 4);
defer headers.deinit(gpa);
try headers.put("Content-Type", "application/json");
try headers.put("Authorization", "Bearer secret");
var response = try client.fetch("https://httpbin.org/post", .{
.method = .post,
.headers = &headers,
.body = "{\"name\":\"Alice\"}",
});
defer response.deinit();
if (response.status() != .ok) return error.UnexpectedStatus;
std.debug.print("{s}\n", .{try response.body() orelse ""});
}
http.Headers.init takes the number of headers it has room for. A non-2xx status isn't an error,
so check the status yourself.
To send JSON, encode it with json.zig or std.json into a buffer
first, and pass that as the body.
Query Strings
The URL is sent as it is, so escape the values you put in a query string:
fn search(client: *http.Client, gpa: std.mem.Allocator, term: []const u8) !void {
const url = try std.fmt.allocPrint(gpa, "https://httpbin.org/get?q={f}", .{
std.fmt.alt(std.Uri.Component{ .raw = term }, .formatEscaped),
});
defer gpa.free(url);
var response = try client.fetch(url, .{});
defer response.deinit();
std.debug.print("{s}\n", .{try response.body() orelse ""});
}
This sends ?q=fish%20%26%20chips.
Redirects
fetch follows up to 10 redirects. .max_redirects changes that for one request. With 0, fetch
doesn't follow redirects and you get the redirect response itself.
Compressed Responses
The client asks for gzip and deflate, and decodes the body for you. Pass .decompress = false to
get the body as it was sent. response.contentEncoding() tells you how it was encoded.
Errors and Timeouts
A request has 30 seconds to finish by default. That covers connecting, the TLS handshake, every
redirect and the whole body. .timeout replaces it for one request, and .timeout = .none turns it
off:
var response = try client.fetch(url, .{
.timeout = .{ .duration = .{ .raw = .fromSeconds(120), .clock = .awake } },
});
A request that runs out of time fails with error.Timeout.
A body over 10 MiB fails with error.ResponseTooLarge. max_response_size in the
client configuration changes the limit. For bigger bodies,
stream the response instead.
Next Steps
- Streaming Responses covers reading the body as it arrives.
- Configuration covers the client settings.