In a previous post I talked about working with JSON in Zig. This is useful, but I also need to send and receive JSON formatted data to HTTP API endpoints in my Zig EspoCRM SDK. In this post I’m going to skip theory an get right into code, let’s start with some scaffolding.
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 (our 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. We do this with a slice of u8 bytes.
var redirect_buffer: [8 * 1024]u8 = undefined;
This buffer accumulates header bytes as the client follows redirects but 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 to complete an HTTP request.
The GET Request #
You’ll notice we added header values to the .extra_headers of the client.fetch() call. You can add/remove headers as required by the endpoint. In this example we’re adding the minimum necessary for the HTTP API endpoint we’re talking to.
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 required to make the call. If we’re going to make multiple HTTP requests, this repetition gets tedious, let’s wrap the complexity and clean up the call site.
Making it reusable #
Notice the .extra_headers, we’re setting it to an empty struct if the caller doesn’t give us headers. This is a good place to set sane defaults.
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 = &[_]std.http.Header{
.{ .name = "User-Agent", .value = "potato/0.0.1" },
.{ .name = "Content-Type", .value = "application/json" },
.{ .name = "Accept", .value = "application/json" },
};
var response = try httpRequest(
allocator,
"https://postman-echo.com/get?key=value",
.GET,
headers,
null,
);
defer response.deinit();
The POST #
const payload =
\\{
\\ "key1": "val1",
\\ "key2": "val2"
\\}
;
const headers = &[_]std.http.Header{
.{ .name = "User-Agent", .value = "potato/0.0.1" },
.{ .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 #
const headers = &[_]std.http.Header{
.{ .name = "User-Agent", .value = "potato/0.0.1" },
.{ .name = "Content-Type", .value = "application/json" },
.{ .name = "Accept", .value = "application/json" },
};
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_get = &[_]std.http.Header{
.{ .name = "User-Agent", .value = "potato/0.0.1" },
.{ .name = "Content-Type", .value = "application/json" },
.{ .name = "Accept", .value = "application/json" },
};
// GET
var get_response = try httpRequest(
allocator,
"https://postman-echo.com/get?key1=val1&key2=val2",
.GET,
headers_get,
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 headers_post = &[_]std.http.Header{
.{ .name = "User-Agent", .value = "potato/0.0.1" },
.{ .name = "Content-Type", .value = "application/json" },
.{ .name = "Accept", .value = "application/json" },
};
var post_response = try httpRequest(
allocator,
"https://jsonplaceholder.typicode.com/posts",
.POST,
headers_post,
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"
\\}
;
const headers_put = &[_]std.http.Header{
.{ .name = "User-Agent", .value = "potato/0.0.1" },
.{ .name = "Content-Type", .value = "application/json" },
.{ .name = "Accept", .value = "application/json" },
};
var put_response = try httpRequest(
allocator,
"https://postman-echo.com/put",
.PUT,
headers_put,
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 });
}