Is this the best way to make a request to some API from Zig?

Just looking for a small review into this piece of code that sends a get request to some API. Is this how it would/should be done or is there a better/more efficient way?

const std = @import("std");
const http = std.http;
const heap = std.heap;

const Client = std.http.Client;
const Headers = std.http.Headers;
const Uri = std.Uri;
const RequestOptions = std.http.Client.RequestOptions;

var gpa_impl = heap.GeneralPurposeAllocator(.{}){};
const gpa = gpa_impl.allocator();

const Req = struct {
    const Self = @This();
    const Allocator = std.mem.Allocator;

    const ReqOptions = struct {
        /// Required
        max: usize,
    };

    allocator: Allocator,
    client: std.http.Client,
    req_options: ReqOptions,

    pub fn init(allocator: Allocator, req_options: ReqOptions) Self {
        const c = Client{ .allocator = allocator };
        return Self{
            .allocator = allocator,
            .client = c,
            .req_options = req_options,
        };
    }

    pub fn deinit(self: *Self) void {
        self.client.deinit();
    }

    /// Blocking
    pub fn get(self: *Self, url: []const u8, headers: Headers, options: RequestOptions) ![]u8 {
        const uri = try Uri.parse(url);

        var req = try self.client.open(http.Method.GET, uri, headers, options);
        defer req.deinit();

        try req.send(.{});
        try req.wait();

        const res = try req.reader().readAllAlloc(self.allocator, self.req_options.max);
        return res;
    }
};

pub fn main() !void {
    const url = "https://jsonplaceholder.typicode.com/todos/1";

    var req = Req.init(gpa, .{ .max = 1024 });
    defer req.deinit();

    var headers = Headers.init(gpa);
    defer headers.deinit();

    const buf = try req.get(url, headers, .{});
    defer req.allocator.free(buf);

    std.debug.print("response - {s}\n", .{buf});
}
1 Like

Hey @Chooky, welcome to the forums!

One nitpick I have for your code example is the global allocator. It’s not that global allocators can’t exist (they certainly can), but I personally prefer to move things like that into the scope they’re being also being deinit’d from (if possible - in this example it certainly is). Personally, I’d move that to main and then have the deinit statement directly below that - keeps everything in the same scope and makes it (in my opinion) more readable.

2 Likes

Yup, that makes sense! cheers for that.

1 Like

One more addendum here. I’d like to point you towards the GPA source code file - you’ll see this in there:

        /// Returns `Check.leak` if there were leaks; `Check.ok` otherwise.
        pub fn deinit(self: *Self) Check {
            const leaks = if (config.safety) self.detectLeaks() else false;
            if (config.retain_metadata) {
                self.freeRetainedMetadata();
            }
            self.large_allocations.deinit(self.backing_allocator);
            self.bucket_node_pool.deinit();
            self.* = undefined;
            return @as(Check, @enumFromInt(@intFromBool(leaks)));
        }

You’ll see that the GPA allocator has it’s own deinit function (it contains state). When you call deinit on the GPA, it will return a value about the attempted deinit call. Here’s the values it can return:

pub const Check = enum { ok, leak };

You can check these values like so:

// needs to be against the gpa_impl - not the allocator interface by calling "allocator()".
if (gpa_impl.deinit() == .leak) // do something (probably call @panic)

It’s good to get into the habit of checking if the GPA tells you that you are leaking memory and you can use that return value to do so :slight_smile:

1 Like

Ooo, interesting. I didn’t include it in the snippet but I was scoping all of main and running

const has_leaked = gpa_impl.detectLeaks();
std.log.debug("Has leaked: {}\n", .{has_leaked});

although, de-initialising the allocator and checking it that way seems like the better way of doing so, thanks for that!

Apart from my allocator use, is there any feedback on the way of sending the request? I guess in some sense I’m attempting to naively implement something like fetch() from Javascript world.

I’ll link someone here - I think our friend @mscott9437 has worked quite a bit with requests.

1 Like

Thanks, I appreciate the help!

1 Like

There’s also simpler fetch api in std.http.client, in case you need TLSv1.2 test std lib TLS implementation against many real world servers · Issue #14172 · ziglang/zig · GitHub I have wrapper for it over curl here Add support for builtin libcurl and some other fixes by Cloudef · Pull Request #4 · jiacai2050/zig-curl · GitHub

1 Like

Yes, I did see that std.http.Client had a fetch() method, although I wasn’t too sure what the difference between that and open() were and which would be better when making a request to some api.

Your code looks about right for how to use the HTTP client. Right now I use this blog post as a reference for a basic client/server model.

As for how you are doing the request, fetch request are all asynchronous by default, whereas it looks like you are sending a blocking request. Also if you really want to implement a way to handle simple fetch requests at this level, then you are missing some things like declaring the MIME type in the headers, also handling different request methods (POST, PUT, etc…). And you also will want some way to handle CORS requests. This MDN article has a good example for making a POST fetch request in JavaScript, under the section “Supplying request options”. i.e. Request method, request mode (cors/non-cors), cache control, supplying credentials, mime type, as well as the redirect and referrer policies. Though I’m not exactly sure how I would implement all of that in Zig, it gives you a good idea of what a fetch request should be able to do.

For making requests to an API then you want to go with a fetch request. I haven’t tried out the Zig implementation (Client.fetch()), but if it’s there then that’s going to be the way to go. API requests are typically made as an AJAX request (async with no page reload) in the browser with JavaScript using XMlHttpRequest (legacy method) or fetch(). Whereas Client.open() is much more broad in nature, and would be more like just making a regular URL request in your browser bar.

1 Like

As for how you are doing the request, fetch request are all asynchronous by default, whereas it looks like you are sending a blocking request.

Yep, I was planning on making this async eventually.

For making requests to an API then you want to go with a fetch request.

I’ll definitely take a look into the fetch() method on std.http.Client then.

Whereas Client.open() is much more broad in nature, and would be more like just making a regular URL request in your browser bar.

Ahhh, okay. That makes sense.

Cheers for this! Helps a ton. I’ll investigate into those things and try a new implementation with std.http.Client.fetch().

1 Like