↓ Skip to main content
  1. Posts/

Making HTTP Requests in Zig 0.16.0 (GET, POST, PUT, DELETE)

definitepotato
Author
definitepotato
Code slinger, golang, ziglang, grokking life, tabletop games enthusiast, obsession with keebs and numbers.
Table of Contents

This post remains relevant with zig 0.17.0

If you’re still on zig 0.15.2, here’s my older post on making HTTP requests.

Concepts to understand before we make HTTP requests
#

The Fetch
#

client.fetch(options) is the request. It connects, sends the request, follows redirects, receives the response, and returns once everything has arrived. The options parameter is a struct with defaults for nearly every field, so a minimal call is short:

const result = try client.fetch(.{
  .location = .{ .url = "https://postman-echo.com/get" },
  .response_writer = &body.writer,
});

A few defaults are worth knowing:

  • .location takes .url or .uri.
  • .method defaults to GET if you don’t provide a payload, POST if you provide one.
  • .response_writer is optional, leave it out and the body is read and discarded, which is fine when you only care about the status.

result is a FetchResult, which holds status.

The Body
#

fetch() doesn’t return the body, it writes it into any *std.Io.Writer. For those “give me the whole body in memory” situations use std.Io.Writer.Allocting, a writer backed by a growable heap buffer.

var body: std.Io.Writer.Allocating = .init(allocator);
defer body.deinit();

_ = try client.fetch(.{ ..., .response_writer = &body.writer });

const text = body.written():
const owned = try body.ToOwnedSlice();

Use written() when you plan to use the body inside the same function, otherwise use toOwnedSlice().

The Headers
#

fetch() has two header fields:

  • .headers controls the six headers the client manages itself: host, authorization, user_agent, connection, accept_encoding, content_type. Each one is .default, .omit, or .{override = "value" }.
  • .extra_headers is a slice of name/value pairs sent verbatim in addition to everything else.

The client always sends its own User-Agent: zig/0.16.0 (std.http). Put your User-Agent in .extra_headers and the request carries two of them. To replace it:

.headers = .{ .user_agent = . { .override = "definitepotato/0.0.1" } },

Rule of thumb is if it is one of those six, use .headers, anything else goes in .extra_headers.

Sending a body
#

.payload is a plain ?[]const u8. The fetch() sets Content-Length from it, but:

  • It never sets Content-Type. The default is to send none at all.
  • It does not follow redirects when there is a payload.

The fail
#

try client.fetch(...) only catches failures where:

  1. No response arrived.
  2. An unparseable URL (error.InvalidFormat).
  3. A host that does not resolve (error.NoAddressReturned).
  4. A refused connection.
  5. A TLS failure.

Connection pooling
#

A std.http.Client keeps a pool of open connections. When the next request goes to a host it already talked to, it reuses the connection and skips the TCP and TLS handshakes.

Let’s write code
#

Method 1: GET
#

const std = @import("std");

pub fn main() !void {
    var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var client: std.http.Client = .{ .allocator = allocator, .io = io };
    defer client.deinit();

    var body: std.Io.Writer.Allocating = .init(allocator);
    defer body.deinit();

    const result = try client.fetch(.{
        .location = .{ .url = "https://postman-echo.com/get?key=value" },
        .method = .GET,
        .response_writer = &body.writer,
    });

    std.debug.print("Status: {d} {t}\n", .{ @intFromEnum(result.status), result.status });
    std.debug.print("Body:   {s}\n", .{body.written()});
}

Method 2: POST and PUT
#

const std = @import("std");

pub fn main() !void {
    var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var client: std.http.Client = .{ .allocator = allocator, .io = io };
    defer client.deinit();

    const payload =
        \\{
        \\  "id": 1,
        \\  "username": "definitepotato",
        \\  "avatar": "definitepotato.png"
        \\}
    ;

    const json_headers: std.http.Client.Request.Headers = .{
        .content_type = .{ .override = "application/json" },
    };

    {
        var body: std.Io.Writer.Allocating = .init(allocator);
        defer body.deinit();

        const result = try client.fetch(.{
            .location = .{ .url = "https://jsonplaceholder.typicode.com/posts" },
            .method = .POST,
            .payload = payload,
            .headers = json_headers,
            .response_writer = &body.writer,
        });
        std.debug.print("POST: {t}\n{s}\n\n", .{ result.status, body.written() });
    }

    {
        var body: std.Io.Writer.Allocating = .init(allocator);
        defer body.deinit();

        const result = try client.fetch(.{
            .location = .{ .url = "https://postman-echo.com/put" },
            .method = .PUT,
            .payload = payload,
            .headers = json_headers,
            .response_writer = &body.writer,
        });
        std.debug.print("PUT: {t}\n{s}\n", .{ result.status, body.written() });
    }

}

Method 3: Reusable HTTP client (GET, POST, PUT, DELETE)
#

const std = @import("std");

const Response = struct {
    status: std.http.Status,
    body: []u8,
    allocator: std.mem.Allocator,

    fn deinit(self: Response) void {
        self.allocator.free(self.body);
    }
};

const RequestOptions = struct {
    method: std.http.Method = .GET,
    payload: ?[]const u8 = null,
    extra_headers: []const std.http.Header = &.{},
};

const user_agent = "definitepotato/0.0.1";

fn request(client: *std.http.Client, url: []const u8, options: RequestOptions) !Response {
    const allocator = client.allocator;

    var body: std.Io.Writer.Allocating = .init(allocator);
    defer body.deinit();

    const result = try client.fetch(.{
        .location = .{ .url = url },
        .method = options.method,
        .payload = options.payload,
        .response_writer = &body.writer,
        .headers = .{
            .user_agent = .{ .override = user_agent },
            .content_type = if (options.payload != null)
                .{ .override = "application/json" }
            else
                .default,
        },
        .extra_headers = options.extra_headers,
    });

    if (result.status.class() != .success) {
        std.debug.print("{s} {s}: {d} {t}\n{s}\n", .{
            @tagName(options.method),    url,
            @intFromEnum(result.status), result.status,
            body.written(),
        });
        return error.HttpRequestFailed;
    }

    return .{
        .status = result.status,
        .body = try body.toOwnedSlice(),
        .allocator = allocator,
    };
}

pub fn main() !void {
    var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var client: std.http.Client = .{ .allocator = allocator, .io = io };
    defer client.deinit();

    const accept_json = &[_]std.http.Header{
        .{ .name = "Accept", .value = "application/json" },
    };

    const payload =
        \\{"id": 1, "username": "definitepotato", "avatar": "definitepotato.png"}
    ;

    {
        const res = try request(&client, "https://postman-echo.com/get?key1=val1&key2=val2", .{
            .extra_headers = accept_json,
        });
        defer res.deinit();
        std.debug.print("GET: {t}\n{s}\n\n", .{ res.status, res.body });
    }

    {
        const res = try request(&client, "https://jsonplaceholder.typicode.com/posts", .{
            .method = .POST,
            .payload = payload,
            .extra_headers = accept_json,
        });
        defer res.deinit();
        std.debug.print("POST: {t}\n{s}\n\n", .{ res.status, res.body });
    }

    {
        const res = try request(&client, "https://postman-echo.com/put", .{
            .method = .PUT,
            .payload = payload,
            .extra_headers = accept_json,
        });
        defer res.deinit();
        std.debug.print("PUT: {t}\n{s}\n\n", .{ res.status, res.body });
    }

    {
        const res = try request(&client, "https://postman-echo.com/delete", .{ .method = .DELETE });
        defer res.deinit();
        std.debug.print("DELETE: {t}\n{s}\n\n", .{ res.status, res.body });
    }

    if (request(&client, "https://postman-echo.com/status/404", .{})) |res| {
        res.deinit();
    } else |err| {
        std.debug.print("-> {s}\n", .{@errorName(err)});
    }
}

Method 4: Serializing JSON
#

const std = @import("std");

const NewPost = struct {
    userId: u32,
    title: []const u8,
    body: []const u8,
};

const Post = struct {
    id: u32,
    userId: u32,
    title: []const u8,
    body: []const u8,
};

pub fn main() !void {
    var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var client: std.http.Client = .{ .allocator = allocator, .io = io };
    defer client.deinit();

    const new_post: NewPost = .{
        .userId = 7,
        .title = "Zig and HTTP",
        .body = "Now with 0.16 and std.Io.",
    };
    const payload = try std.json.Stringify.valueAlloc(allocator, new_post, .{});
    defer allocator.free(payload);
    std.debug.print("sending:  {s}\n", .{payload});

    var response_body: std.Io.Writer.Allocating = .init(allocator);
    defer response_body.deinit();

    const result = try client.fetch(.{
        .location = .{ .url = "https://jsonplaceholder.typicode.com/posts" },
        .method = .POST,
        .payload = payload,
        .headers = .{ .content_type = .{ .override = "application/json" } },
        .response_writer = &response_body.writer,
    });
    if (result.status.class() != .success) return error.HttpRequestFailed;
    std.debug.print("received: {s}\n", .{response_body.written()});

    const parsed = try std.json.parseFromSlice(
        Post,
        allocator,
        response_body.written(),
        .{ .ignore_unknown_fields = true },
    );
    defer parsed.deinit();

    const post = parsed.value;
    std.debug.print("\nserver assigned id {d} to \"{s}\" by user {d}\n", .{
        post.id, post.title, post.userId,
    });
}

Related