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], '}');
}