Skip to main content
  1. Posts/

Zig and HTTP

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

In a previous post I talked about working with JSON in Zig. This is useful, but I needed to do more. I needed to send and receive JSON formatted data to HTTP API endpoints in my Zig EspoCRM SDK. Let’s start with the simplest approach:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

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

    const uri = try std.Uri.parse("https://postman-echo.com/get");
}

We’ve got a client and a uri. But how do we actually make the request?

The fetch method
#

The standard library provides client.fetch(). This method makes the actual request to the parsed uri (endpoint):

const result = try client.fetch(.{
    .location = .{ .uri = uri },
    .method = .GET,
    .redirect_buffer = &redirect_buffer,
    .response_writer = &body.writer,
    .extra_headers = &[_]std.http.Header{},
    .payload = null,
});

Before we get into handling the response, let’s first break down redirect_buffer and response_writer. HTTP redirects are handled automatically, but you need to provide a buffer for the response headers:

var redirect_buffer: [8 * 1024]u8 = undefined;

This buffer accumulates header bytes as the client follows redirects.

To capture the response body, we need to provide a writer that implements the std.Io.Writer interface:

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

The std.Io.Writer.Allocating type is a growable buffer that allocates memory as needed. We give it some initial capacity (in our case 64 bytes) to avoid immediate reallocation. With that, we finally have enough knowledge to complete an HTTP request.

The GET Request
#

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

    var redirect_buffer: [8 * 1024]u8 = undefined;
    var body: std.Io.Writer.Allocating = .init(allocator);
    defer body.deinit();
    try body.ensureUnusedCapacity(64);

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

    const uri = try std.Uri.parse("https://postman-echo.com/get?key=value");

    const result = try client.fetch(.{
        .location = .{ .uri = uri },
        .method = .GET,
        .redirect_buffer = &redirect_buffer,
        .response_writer = &body.writer,
        .extra_headers = &[_]std.http.Header{
            .{ .name = "User-Agent", .value = "potato/0.0.1" },
            .{ .name = "Accept", .value = "application/json" },
        },
        .payload = null,
    });

    const response_body = try body.toOwnedSlice();
    defer allocator.free(response_body);

    std.debug.print("Status: {}\n", .{result.status});
    std.debug.print("Body: {s}\n", .{response_body});
}

This works, but look at all that setup. If we’re going to make multiple HTTP requests, this repetition gets tedious. We can wrap the complexity:

Making it reusable
#

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

    pub fn deinit(self: *HttpResponse) void {
        self.allocator.free(self.body);
    }
};

fn httpRequest(
    allocator: std.mem.Allocator,
    uri: []const u8,
    method: std.http.Method,
    headers: ?[]const std.http.Header,
    payload: ?[]const u8,
) !HttpResponse {
    const parsed_uri = try std.Uri.parse(uri);

    var redirect_buffer: [8 * 1024]u8 = undefined;
    var body: std.Io.Writer.Allocating = .init(allocator);
    defer body.deinit();
    try body.ensureUnusedCapacity(64);

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

    const result = try client.fetch(.{
        .location = .{ .uri = parsed_uri },
        .method = method,
        .redirect_buffer = &redirect_buffer,
        .response_writer = &body.writer,
        .extra_headers = headers orelse &[_]std.http.Header{},
        .payload = payload,
    });

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

Using it for different methods
#

With the helper function, making requests becomes cleaner:

The GET
#

const headers_get = &[_]std.http.Header{
    .{ .name = "User-Agent", .value = "potato/0.0.1" },
    .{ .name = "Accept", .value = "application/json" },
};

var response = try httpRequest(
    allocator,
    "https://postman-echo.com/get?key=value",
    .GET,
    headers_get,
    null,
);
defer response.deinit();

The POST
#

const payload =
    \\{
    \\  "key1": "val1",
    \\  "key2": "val2"
    \\}
;

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

var response = try httpRequest(
    allocator,
    "https://jsonplaceholder.typicode.com/posts",
    .POST,
    headers,
    payload,
);
defer response.deinit();

The PUT
#

var response = try httpRequest(
    allocator,
    "https://postman-echo.com/posts/1",
    .PUT,
    headers,
    payload,  // Updated data
);
defer response.deinit();

The DELETE
#

var response = try httpRequest(
    allocator,
    "https://postman-echo.com/delete",
    .DELETE,
    null,  // Usually no headers needed
    null,  // No body
);
defer response.deinit();

Complete example
#

const std = @import("std");

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

    pub fn deinit(self: *HttpResponse) void {
        self.allocator.free(self.body);
    }
};

fn httpRequest(
    allocator: std.mem.Allocator,
    uri: []const u8,
    method: std.http.Method,
    headers: ?[]const std.http.Header,
    payload: ?[]const u8,
) !HttpResponse {
    const parsed_uri = try std.Uri.parse(uri);

    var redirect_buffer: [8 * 1024]u8 = undefined;
    var body: std.Io.Writer.Allocating = .init(allocator);
    defer body.deinit();
    try body.ensureUnusedCapacity(64);

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

    const result = try client.fetch(.{
        .location = .{ .uri = parsed_uri },
        .method = method,
        .redirect_buffer = &redirect_buffer,
        .response_writer = &body.writer,
        .extra_headers = headers orelse &[_]std.http.Header{},
        .payload = payload,
    });

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

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

    const headers = &[_]std.http.Header{
        .{ .name = "User-Agent", .value = "potato/0.0.1" },
        .{ .name = "Accept", .value = "application/json" },
    };

    // GET
    var get_response = try httpRequest(
        allocator,
        "https://postman-echo.com/get?key1=val1&key2=val2",
        .GET,
        headers,
        null,
    );
    defer get_response.deinit();
    std.debug.print("GET: {}\n{s}\n\n", .{ get_response.status, get_response.body });

    // POST
    const post_payload =
        \\{
        \\  "key1": "val1",
        \\  "key2": "val2"
        \\}
    ;

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

    var post_response = try httpRequest(
        allocator,
        "https://jsonplaceholder.typicode.com/posts",
        .POST,
        post_headers,
        post_payload,
    );
    defer post_response.deinit();
    std.debug.print("POST: {}\n{s}\n\n", .{ post_response.status, post_response.body });

    // PUT
    const put_payload =
        \\{
        \\  "id": 1,
        \\  "username": "potato",
        \\  "avatar": "potato.png"
        \\}
    ;

    var put_response = try httpRequest(
        allocator,
        "https://postman-echo.com/put",
        .PUT,
        post_headers,
        put_payload,
    );
    defer put_response.deinit();
    std.debug.print("PUT: {}\n{s}\n\n", .{ put_response.status, put_response.body });

    // DELETE
    var delete_response = try httpRequest(
        allocator,
        "https://postman-echo.com/delete",
        .DELETE,
        null,
        null,
    );
    defer delete_response.deinit();
    std.debug.print("DELETE: {}\n{s}\n", .{ delete_response.status, delete_response.body });
}

Related