I finally figured out Zig's tls.Client
The new Zig 0.15.1’s std.Io interface is gnarly. When you see the performance gains you can get when using these new Reader and Writer interfaces, you begin to soften on why every goddamn thing needs a buffer. Then the flushing. My God the flushing. I was stuck for two days trying to use the new tls.Client in Zig 0.15.2. Why not just use the std.http.Client you ask? I needed to get a bit more custom. To tell you the truth, the tls.Client is way too opaque and high-level for my needs, but let’s take it a step at a time.
I’d used the resources I blogged about earlier in the week to put together this small piece of code to write to a tls server. It would do a basic HTTP GET, that’s it. Little did I know I’d take two days to figure out. Here’s the first piece of code I wrote:
 1const std = @import("std");
 2
 3pub fn main() !void {
 4    var gpa = std.heap.DebugAllocator(.{}){};
 5    defer std.debug.assert(gpa.deinit() == .ok);
 6    const allocator = gpa.allocator();
 7
 8    const hostname = "sheran.sg";
 9
10    var conn = try std.net.tcpConnectToHost(allocator, hostname, 443);
11    defer conn.close();
12
13    var read_buf: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined;
14    var write_buf: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined;
15
16    var reader = conn.reader(&read_buf);
17    var writer = conn.writer(&write_buf);
18
19    var orb: [8192]u8 = undefined;
20    var owb: [8192]u8 = undefined;
21
22    var bundle = std.crypto.Certificate.Bundle{};
23    try bundle.rescan(allocator);
24    defer bundle.deinit(allocator);
25
26    const options: std.crypto.tls.Client.Options = .{
27        .write_buffer = &owb,
28        .read_buffer = &orb,
29        .host = .{ .explicit = hostname },
30        .ca = .{ .bundle = bundle },
31    };
32
33    var tls_client: std.crypto.tls.Client = try std.crypto.tls.Client.init(reader.interface(), &writer.interface, options);
34
35    var stdout = std.fs.File.stdout().writer(&.{}).interface;
36
37    try tls_client.writer.print("GET / HTTP/1.1\r\n", .{});
38    try tls_client.writer.print("Host: {s}\r\n", .{hostname});
39    try tls_client.writer.print("Connection: Close\r\n", .{});
40    try tls_client.writer.print("\r\n", .{});
41
42    try tls_client.writer.flush();
43
44    var bytesread: usize = 0;
45    while (true) {
46        const read = tls_client.reader.stream(&stdout, .unlimited) catch |err| switch (err) {
47            error.EndOfStream => break,
48            else => |e| return e,
49        };
50        bytesread += read;
51    }
52    try tls_client.end();
53}Seemed kinda reasonable. I’d follow Andrew’s advice and not forget to flush, but this code did not work. Why? Because I didn’t flush the tls_client’s encrypted stream as well (.output) . I had only flushed the plaintext stream. So I just had to add that additional line of code and the code behaved as I wanted it to. Here is the working code:
 1const std = @import("std");
 2
 3pub fn main() !void {
 4    var gpa = std.heap.DebugAllocator(.{}){};
 5    defer std.debug.assert(gpa.deinit() == .ok);
 6    const allocator = gpa.allocator();
 7
 8    const hostname = "sheran.sg";
 9
10    var conn = try std.net.tcpConnectToHost(allocator, hostname, 443);
11    defer conn.close();
12
13    var read_buf: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined;
14    var write_buf: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined;
15
16    var reader = conn.reader(&read_buf);
17    var writer = conn.writer(&write_buf);
18
19    var orb: [8192]u8 = undefined;
20    var owb: [8192]u8 = undefined;
21
22    var bundle = std.crypto.Certificate.Bundle{};
23    try bundle.rescan(allocator);
24    defer bundle.deinit(allocator);
25
26    const options: std.crypto.tls.Client.Options = .{
27        .write_buffer = &owb,
28        .read_buffer = &orb,
29        .host = .{ .explicit = hostname },
30        .ca = .{ .bundle = bundle },
31    };
32
33    var tls_client: std.crypto.tls.Client = try std.crypto.tls.Client.init(reader.interface(), &writer.interface, options);
34
35    var stdout = std.fs.File.stdout().writer(&.{}).interface;
36
37    try tls_client.writer.print("GET / HTTP/1.1\r\n", .{});
38    try tls_client.writer.print("Host: {s}\r\n", .{hostname});
39    try tls_client.writer.print("Connection: Close\r\n", .{});
40    try tls_client.writer.print("\r\n", .{});
41
42    try tls_client.writer.flush(); // flush the plaintext writer
43    try tls_client.output.flush(); // flush the encrypted writer
44
45    var bytesread: usize = 0;
46    while (true) {
47        const read = tls_client.reader.stream(&stdout, .unlimited) catch |err| switch (err) {
48            error.EndOfStream => break,
49            else => |e| return e,
50        };
51        bytesread += read;
52    }
53    try tls_client.end();
54}