Http request hangs when receiving head without flushing conn

Hi all. It feels weird that i have to flush the connection, I thought end(), which calls flush() under the hood would do this. Just checking if I am missing something.

pub const Writer = struct {
    bodyWriter: http.BodyWriter,
    request: *Request,

    pub fn write(self: *Writer, args: struct {
        bytes: []const u8,
    }) !usize {
        return try self.bodyWriter.writer.write(args.bytes);
    }

    pub fn reader(self: *Writer, args: struct {
        reader_buffer: []u8 = &[_]u8{},
        redirect_buffer: []u8 = &[_]u8{},
    }) !Reader {
        try self.bodyWriter.end();
        try self.request.connection.?.flush(); // if i remove this, receiveHead hangs
        var response = try self.request.receiveHead(args.redirect_buffer);
        try checkStatus(response.head.status);
        return Reader{
            .response = response,
            .reader = response.reader(args.reader_buffer),
            .redirect_buffer = args.redirect_buffer,
        };
    }
};

Full code

const std = @import("std");
const Allocator = std.mem.Allocator;
const Io = std.Io;
const http = std.http;
const testing = std.testing;
const Uri = std.Uri;
const assert = std.debug.assert;
const Request = http.Client.Request;
const Bench = @import("Bench.zig");

const HttpClient = @This();
/// http or https
scheme: []const u8,
host: []const u8,
port: ?u16,
client: http.Client,

pub fn init(args: struct {
    allocator: Allocator,
    io: Io,
    endpoint: []const u8,
}) !HttpClient {
    const uri: Uri = try .parse(args.endpoint);
    assert(std.mem.eql(u8, uri.path.percent_encoded, ""));
    return .{
        .scheme = uri.scheme,
        .host = uri.host.?.percent_encoded,
        .port = uri.port,
        .client = .{
            .allocator = args.allocator,
            .io = args.io,
        },
    };
}

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

/// Remember to defer deinit on returned request
pub fn request(self: *HttpClient, args: struct {
    method: http.Method,
    encoded_path: []const u8,
    standard_headers: Request.Headers = .{},
    custom_headers: []const http.Header = &[_]http.Header{},
    transfer_encoding: Request.TransferEncoding = .none,
    nr_redirects_allowed: u16 = 0,
}) !Request {
    const uri: Uri = .{
        .scheme = self.scheme,
        .host = .{ .percent_encoded = self.host },
        .port = self.port,
        .path = .{ .percent_encoded = args.encoded_path },
    };
    var req = try self.client.request(args.method, uri, .{
        .redirect_behavior = .init(args.nr_redirects_allowed),
        .headers = args.standard_headers,
        .extra_headers = args.custom_headers,
    });
    req.transfer_encoding = args.transfer_encoding;
    return req;
}

fn checkStatus(status: http.Status) !void {
    if (status != .ok) {
        return switch (status) {
            .not_found => error.NotFound,
            .method_not_allowed => error.MethodNotAllowed,
            .unauthorized => error.Unauthorized,
            .forbidden => error.Forbidden,
            .bad_request => error.BadRequest,
            .precondition_failed => error.PreconditionFailed,
            .internal_server_error => error.InternalServerError,
            .not_implemented => error.NotImplemented,
            else => error.UnexpectedStatusCode,
        };
    }
}
/// Use if no request body is sent, otherwise use writer.
pub fn reader(_: *HttpClient, args: struct {
    request: *Request,
    buffer: []u8 = &[_]u8{},
    redirect_buffer: []u8 = &[_]u8{},
}) !Reader {
    try args.request.sendBodiless();
    var response = try args.request.receiveHead(args.redirect_buffer);
    try checkStatus(response.head.status);
    const reader_ptr = response.reader(args.buffer);

    return Reader{
        .redirect_buffer = args.redirect_buffer,
        .response = response,
        .reader = reader_ptr,
    };
}

pub const Reader = struct {
    response: http.Client.Response,
    reader: *Io.Reader,
    redirect_buffer: []u8,

    /// Remember to defer allocator.free(bytes)
    pub fn read(self: *Reader, args: struct {
        allocator: Allocator,
        /// recommended to set to content-length if available
        length: usize,
    }) ![]const u8 {
        return try self.reader.readAlloc(args.allocator, args.length);
    }
};

/// Only works for POST, PATCH and PUT
pub fn writer(_: *HttpClient, args: struct {
    request: *Request,
    buffer: []u8 = &[_]u8{},
}) !Writer {
    assert(args.request.method.requestHasBody());
    assert(args.request.transfer_encoding != .none);
    return Writer{
        .bodyWriter = try args.request.sendBodyUnflushed(args.buffer),
        .request = args.request,
    };
}

pub const Writer = struct {
    bodyWriter: http.BodyWriter,
    request: *Request,

    pub fn write(self: *Writer, args: struct {
        bytes: []const u8,
    }) !usize {
        return try self.bodyWriter.writer.write(args.bytes);
    }

    pub fn reader(self: *Writer, args: struct {
        reader_buffer: []u8 = &[_]u8{},
        redirect_buffer: []u8 = &[_]u8{},
    }) !Reader {
        try self.bodyWriter.end();
        try self.request.connection.?.flush(); // if i remove this, receiveHead hangs
        var response = try self.request.receiveHead(args.redirect_buffer);
        try checkStatus(response.head.status);
        return Reader{
            .response = response,
            .reader = response.reader(args.reader_buffer),
            .redirect_buffer = args.redirect_buffer,
        };
    }
};

// Tests
var bench: Bench = .init(.{});

fn testClient() !HttpClient {
    return try HttpClient.init(.{
        .allocator = testing.allocator,
        .io = testing.io,
        .endpoint = "https://httpbin.org",
    });
}

test "init" {
    const localhost_client: HttpClient = try .init(.{
        .allocator = testing.allocator,
        .io = testing.io,
        .endpoint = "http://localhost:8080",
    });
    try testing.expectEqualStrings("http", localhost_client.scheme);
    try testing.expectEqualStrings("localhost", localhost_client.host);
    try testing.expectEqual(8080, localhost_client.port.?);
    var test_client = try testClient();
    defer test_client.deinit();
    try testing.expectEqualStrings("https", test_client.scheme);
    try testing.expectEqualStrings("httpbin.org", test_client.host);
    try testing.expectEqual(null, test_client.port);
}

test "get" {
    var test_client = try testClient();
    defer test_client.deinit();
    var req = try test_client.request(.{
        .method = .GET,
        .encoded_path = "/get",
        .standard_headers = .{ .content_type = .{ .override = "application/json" } },
    });
    defer req.deinit();
    var bin_reader = try test_client.reader(.{
        .request = &req,
    });
    const body = try bin_reader.read(.{
        .allocator = testing.allocator,
        .length = bin_reader.response.head.content_length.?,
    });
    defer testing.allocator.free(body);
    try testing.expectEqual(body[0], '{');
    try testing.expectEqual(body[body.len - 2], '}');
}

test "post" {
    var test_client = try testClient();
    defer test_client.deinit();
    var req = try test_client.request(.{
        .method = .POST,
        .encoded_path = "/post",
        .standard_headers = .{ .content_type = .{ .override = "application/json" } },
        .transfer_encoding = .{ .content_length = 17 },
    });
    defer req.deinit();
    var bin_writer = try test_client.writer(.{
        .request = &req,
        .buffer = &.{},
    });
    _ = try bin_writer.write(.{
        .bytes = "{\"hello\":",
    });
    _ = try bin_writer.write(.{
        .bytes = "\"world\"}",
    });
    var readr = try bin_writer.reader(.{});
    const body = try readr.read(.{
        .allocator = testing.allocator,
        .length = readr.response.head.content_length.?,
    });
    defer testing.allocator.free(body);
    std.debug.print("body: {s}\n", .{body});
    try testing.expectEqual(body[0], '{');
    try testing.expectEqual(body[body.len - 2], '}');
}

It was in fact weird and the solution was simple. This failed previously because i only had bodyWriter.flush() without the preceeding bodyWriter.end(), which also just hang.

But if anyone has any comments on other weirdness or room for improvement please mention them, I am still wrapping my head around the http.Client.

1 Like