std.http.Client not working as expected

Heyo,
so I sadly cant show the code as im trying to make a home automation “script” in zig and it involves a bit of private contact information.

Basically the program looks roughly as follows (-> main is pseudocode but the the two http functions are the original code):

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

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

    while (downloading) {
        try download(&client, "my url", target_dir, "a struct with a function that generates the filename and verifies that the download is correct");
    }
    try sendSignalMessage(&client, "api uri", "my number", "recipient", "some message");
}

// this is not pseudocode but an exact copy
fn download(
    client: *std.http.Client,
    uri: std.Uri,
    dir: std.fs.Dir,
    generator: anytype,
) ![]const u8 {
    var server_header_buffer: [16 * 1024]u8 = undefined;
    var req = try client.open(.GET, uri, .{ .server_header_buffer = &server_header_buffer });
    defer req.deinit();

    try req.send();
    try req.finish();
    try req.wait();

    switch (req.response.status.class()) {
        .client_error, .server_error => return error.InvalidResponseStatus,
        else => {},
    }

    const filename = try generator.generate(req.response.content_disposition);

    const file = try dir.createFile(filename, .{});
    defer file.close();
    var fifo: std.fifo.LinearFifo(u8, .{ .Static = 4 * 1024 }) = .init();
    try fifo.pump(req.reader(), file.writer());

    return filename;
}

// also an exact copy of the original code
fn sendSignalMesage(
    client: *std.http.Client,
    uri: std.Uri,
    host_number: []const u8,
    message: []const u8,
    recipients: []const []const u8,
) !void {
    std.debug.assert(recipients.len == 1); // TODO

    const fmt_str =
        \\{{
        \\    "message": "{s}",
        \\    "number": "{s}",
        \\    "recipients": [ "{s}" ]
        \\}}
    ;

    var server_header_buffer: [16 * 1024]u8 = undefined;
    var req = try client.open(.POST, uri, .{
        .server_header_buffer = &server_header_buffer,
        .headers = .{
            .accept_encoding = .{ .override = "application/json" },
            .content_type = .{ .override = "application/json" },
        },
    });
    defer req.deinit();
    req.transfer_encoding = .{
        .content_length = std.fmt.count(fmt_str, .{ message, host_number, recipients[0] }),
    };

    try req.send();

    var bufwriter = std.io.bufferedWriter(req.writer());
    try bufwriter.writer().print(fmt_str, .{ message, host_number, recipients[0] });
    try bufwriter.flush();

    try req.finish();
    try req.wait();

    switch (req.response.status.class()) {
        .client_error, .server_error => return error.InvalidResponseStatus,
        else => {},
    }
}

Now the error I am getting is that the signal api claims that my request is malformed, but that only happens when I send the signal message AFTER having used the download method. So as far as I can tell, the http client is somehow being corrupted or im using the requests wrong. Interestingly, if I try to deinit and reinit the client manually right before calling sendSignalMessage, it gives me an error claiming that a connection is still active. How can this be?

I think you could turn that into a test case, do it once with download+signal and once in a new client with only signal and then compare the std.http.Client instances before and after the signal request for equality, either using std.testing.expectEqual or with a method doing your own introspection of the value.

If they aren’t equal hopefully the difference in data would lead towards a specific field that retains state / doesn’t get reset properly.

1 Like

Thats a really smart idea ill try that in a sec

1 Like

Okay so the clients are literally identical, not a single byte changed about them when using expectEqual and expectEqualDeep doesnt support untagged unions

I am just guessing here but maybe it is somehow an issue with the api not handling connection reuse correctly?

Maybe time to use some kind of https://en.wikipedia.org/wiki/Packet_analyzer to see if there is any difference in what is being sent to the API-Server.

Thats the thing, I am not very familiar with http but i suspect that thats the issue too, ill look into it!

1 Like

Okay more things I noticed: Sending a message does not break download, but download breaks sending a message.

The problem was that the message I was sending wasnt terminated using \r\n :sob: I fucking hate windows stuff so much

Edit: nvm, the signal api thingy just only accepts \\n and not \n

I hate Windows as much as anybody else, but in this case your hatred is misplaced… This is how Wikipedia summarizes how headers are sent:

Request syntax: … zero or more request header fields (at least 1 or more headers in case of HTTP/1.1), each consisting of the case-insensitive field name, a colon, optional leading whitespace, the field value, an optional trailing whitespace and ending with a carriage return and a line feed.

2 Likes