Yet Another Example of a HTTP client, but with more features than usual

This is a demo program. You easily find a lot of examples of HTTP clients using the std.http in the standard library. This example here is to show a few more things: adding HTTP fields in the header, using the header fields in the response, authenticating with HTTP basic authentication, sending a body with the HTTP request, formatting and parsing JSON. It uses only the standard library.

It was developed during the IETF 123 hackathon, as a RPP client (but you don’t need to understand RPP). This code is a very small and limited (demo only) RPP client, you need a server such as the one developed during the hackathon.

Tested with Zig 0.14.1 (another common problem with Zig is that the many examples you find online are often outdated and, because Zig is not stable, no longer work).

// The parts of the standard library we use
const std = @import("std");
const http = std.http; // <https://ziglang.org/documentation/0.14.1/std/#std.http.Client>
const Encoder = std.base64.Base64Encoder;
const Uri = std.Uri;
const json = std.json;
const ArrayList = std.ArrayList;
const mem = std.mem;

// The endpoint of API we use
const ref_url = "http://localhost:8080/domains/";

// Some values that you probably should not change
const headers_max_size = 1024;
const body_max_size = 65536;
const jsonoption = json.StringifyOptions{ .whitespace = .minified };
const transaction_id = "123456"; // Would be better to use a random number

// Types. Here, our dictionaries, which will be serialized to
// JSON. Optional fields of the JSON object are represented as
// optionals.
const Domain = struct { holder: u32, tech: u32, admin: u32 };
const Result = struct { result: []u8, status_code: u32, status_message: []u8, holder: ?u32 = null, tech_contact: ?u32 = null, admin_contact: ?u32 = null, registrar: ?u32 = null, created: ?[]u8 = null };

pub fn main() !void {

    // Command-line arguments
    var domain: []const u8 = undefined;
    var contact: ?u32 = null;
    var loginpassword: ?[]const u8 = null;
    var method: http.Method = http.Method.GET;
    var data: Domain = undefined;

    // We need an allocator to create a std.http.Client and for many
    // other things. For simplicity, we use only one allocator, named gpa.
    var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa_impl.deinit();
    const gpa = gpa_impl.allocator();

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

    // Buffer for the HTTP received headers
    var hbuffer: [headers_max_size]u8 = undefined;

    // Process command line
    const args = try std.process.argsAlloc(gpa);
    defer std.process.argsFree(gpa, args);
    if (args.len < 2 or args.len > 5) {
        std.debug.print("Usage: rpp-client domain [login:password] [method] [JSON-to-send]\n", .{});
        return error.WrongArgs;
    }
    domain = args[1];
    if (args.len > 2) {
        if (args[2].len != 0) {
            loginpassword = args[2];
        }
        // else ignore it.
        if (args.len > 3) {
            if (mem.eql(u8, args[3], "POST")) {
                method = http.Method.POST;
            } else if (mem.eql(u8, args[3], "PUT")) {
                method = http.Method.PUT;
            } else if (mem.eql(u8, args[3], "DELETE")) {
                method = http.Method.DELETE;
            } else if (mem.eql(u8, args[3], "HEAD")) {
                method = http.Method.HEAD;
            } else if (mem.eql(u8, args[3], "GET")) {
                method = http.Method.GET;
            } else {
                return error.UnknownMethod; // It would be nice to
                // return the name of the
                // rejected method
            }
            if (args.len > 4) {
                const c = try std.fmt.parseInt(u32, args[4], 10);
                contact = c;
                data = .{ .holder = c, .tech = c, .admin = c };
            }
        }
    }

    // Append the domain name to the base URL
    var fullurl = try gpa.alloc(u8, ref_url.len + domain.len);
    defer gpa.free(fullurl);
    mem.copyForwards(u8, fullurl[0..], ref_url);
    mem.copyForwards(u8, fullurl[ref_url.len..], domain);
    const url = try Uri.parse(fullurl);

    // Create the required headers
    var headers = ArrayList(http.Header).init(gpa);
    defer headers.deinit();
    try headers.append(http.Header{ .name = "Accept", .value = "application/rpp+json" });
    try headers.append(http.Header{ .name = "User-Agent", .value = "RPPafnicClient/0.0" });
    try headers.append(http.Header{ .name = "RPP-cltrid", .value = transaction_id });

    // Compute the authorization value. We use HTTP Basic Authentication.
    var auth: []u8 = undefined;
    if (loginpassword != null) {
        const lp = loginpassword orelse return error.ShouldNotHappen;
        const pad_with_equal: ?u8 = 61;
        const encoder = Encoder.init(std.base64.url_safe_alphabet_chars, pad_with_equal);
        const dest = try gpa.alloc(u8, encoder.calcSize(lp.len));
        defer gpa.free(dest);
        _ = Encoder.encode(&encoder, dest, lp);
        const prefix = "Basic ";
        auth = try gpa.alloc(u8, prefix.len + dest.len);
        mem.copyForwards(u8, auth[0..], prefix);
        mem.copyForwards(u8, auth[prefix.len..], dest);
        try headers.append(http.Header{ .name = "Authorization", .value = auth });
    }

    var payload = ArrayList(u8).init(gpa);
    defer payload.deinit();
    const options = http.Client.RequestOptions{ .server_header_buffer = &hbuffer, .extra_headers = headers.items };

    // Call the API endpoint
    if (method == http.Method.PUT or method == http.Method.POST) {
        if (contact == null) {
            return error.ContactRequested;
        }
        // Format the input data to JSON
        _ = try json.fmt(data, jsonoption).format("", .{}, payload.writer());
        if (payload.items.len == 0) {
            return error.ThisMethodRequiresBody;
        }
    }
    // Connect to the HTTP server and send the request
    var request = try client.open(method, url, options);
    if (method == http.Method.PUT or method == http.Method.POST) {
        request.transfer_encoding = .{ .content_length = payload.items.len };
    }
    defer request.deinit();
    _ = try request.send();
    if (method == http.Method.PUT or method == http.Method.POST) {
        _ = try request.writeAll(payload.items);
    }
    _ = try request.finish();
    _ = try request.wait();

    // Display the HTTP return code
    std.debug.print("HTTP Status code: {any}\n", .{request.response.status}); // Can be std.http.Status.ok, accepted, deleted, etc.

    // Iterate over the headers
    var rheaders = http.Client.Response.iterateHeaders(request.response);
    var has_json = false;
    while (true) {
        const phdr = rheaders.next() orelse break;
        if (mem.eql(u8, phdr.name, "Content-Type")) {
            if (mem.eql(u8, phdr.value, "application/rpp+json") or mem.eql(u8, phdr.value, "application/json")) {
                has_json = true;
            }
        } else if (mem.eql(u8, phdr.name, "RPP-Svtrid")) {
            std.debug.print("Server transaction ID: {s}\n", .{phdr.value});
        }
        // else: ignore the other headers
    }

    // Read the body
    var bbuffer: [body_max_size]u8 = undefined;
    _ = try request.readAll(&bbuffer);
    const blength = request.response.content_length orelse return error.NoBodyLength; // We trust
    // the Content-Length returned by the server…

    // Display the result by parsing the JSON, if there is one
    if (method != http.Method.HEAD and blength > 0 and has_json) {
        var parsed = try json.parseFromSlice(Result, gpa, bbuffer[0..blength], .{});
        defer parsed.deinit();
        const root = parsed.value;
        std.debug.print("Result: {s}\n", .{root.result});
    }

    // Free some stuff
    if (loginpassword != null) {
        gpa.free(auth);
    }
}

Why is this in help? It seems like a showcase.

I thought it was too simple for “Showcase”. Also, I was sure I would gather some feedback (I don’t claim to be a Zig expert).