Skip to content

Making Requests

Dusty has an HTTP client too. Create one http.Client and use it for the whole program:

var client = http.Client.init(init.gpa, init.io, .{});
defer client.deinit();

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