aboutsummaryrefslogtreecommitdiff
path: root/src/http_client.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/http_client.zig')
-rw-r--r--src/http_client.zig302
1 files changed, 214 insertions, 88 deletions
diff --git a/src/http_client.zig b/src/http_client.zig
index 111656492..c14e5d1c2 100644
--- a/src/http_client.zig
+++ b/src/http_client.zig
@@ -1,3 +1,6 @@
+// @link "/Users/jarred/Code/bun/src/deps/zlib/libz.a"
+// @link "/Users/jarred/Code/bun/src/deps/picohttpparser.o"
+
const picohttp = @import("picohttp");
usingnamespace @import("./global.zig");
const std = @import("std");
@@ -6,7 +9,7 @@ const URL = @import("./query_string_map.zig").URL;
const Method = @import("./http.zig").Method;
const iguanaTLS = @import("iguanaTLS");
const Api = @import("./api/schema.zig").Api;
-
+const Lock = @import("./lock.zig").Lock;
const HTTPClient = @This();
const SOCKET_FLAGS = os.SOCK_CLOEXEC;
@@ -35,7 +38,8 @@ header_entries: Headers.Entries,
header_buf: string,
url: URL,
allocator: *std.mem.Allocator,
-verbose: bool = false,
+verbose: bool = isTest,
+tcp_client: tcp.Client = undefined,
pub fn init(allocator: *std.mem.Allocator, method: Method, url: URL, header_entries: Headers.Entries, header_buf: string) HTTPClient {
return HTTPClient{
@@ -79,13 +83,23 @@ fn hashHeaderName(name: string) u64 {
const host_header_hash = hashHeaderName("Host");
const connection_header_hash = hashHeaderName("Connection");
+pub const Encoding = enum {
+ identity,
+ gzip,
+ deflate,
+ brotli,
+};
+
const content_encoding_hash = hashHeaderName("Content-Encoding");
+
const host_header_name = "Host";
const content_length_header_name = "Content-Length";
const content_length_header_hash = hashHeaderName("Content-Length");
const connection_header = picohttp.Header{ .name = "Connection", .value = "close" };
const accept_header = picohttp.Header{ .name = "Accept", .value = "*/*" };
const accept_header_hash = hashHeaderName("Accept");
+const accept_encoding_header = picohttp.Header{ .name = "Accept-Encoding", .value = "deflate, gzip" };
+const accept_encoding_header_hash = hashHeaderName("Accept-Encoding");
const user_agent_header = picohttp.Header{ .name = "User-Agent", .value = "Bun.js " ++ Global.package_json_version };
const user_agent_header_hash = hashHeaderName("User-Agent");
@@ -99,11 +113,13 @@ pub fn buildRequest(this: *const HTTPClient, body_len: usize) picohttp.Request {
var header_names = header_entries.items(.name);
var header_values = header_entries.items(.value);
+ var override_accept_encoding = false;
+
var override_user_agent = false;
for (header_names) |head, i| {
const name = this.headerStr(head);
// Hash it as lowercase
- const hash = hashHeaderName(request_headers_buf[header_count].name);
+ const hash = hashHeaderName(name);
// Skip host and connection header
// we manage those
@@ -111,13 +127,14 @@ pub fn buildRequest(this: *const HTTPClient, body_len: usize) picohttp.Request {
host_header_hash,
connection_header_hash,
content_length_header_hash,
- accept_header_hash,
=> continue,
else => {},
}
override_user_agent = override_user_agent or hash == user_agent_header_hash;
+ override_accept_encoding = override_accept_encoding or hash == accept_encoding_header_hash;
+
request_headers_buf[header_count] = picohttp.Header{
.name = name,
.value = this.headerStr(header_values[i]),
@@ -156,6 +173,11 @@ pub fn buildRequest(this: *const HTTPClient, body_len: usize) picohttp.Request {
};
header_count += 1;
+ if (!override_accept_encoding) {
+ request_headers_buf[header_count] = accept_encoding_header;
+ header_count += 1;
+ }
+
if (body_len > 0) {
request_headers_buf[header_count] = picohttp.Header{
.name = content_length_header_name,
@@ -209,15 +231,15 @@ pub inline fn send(this: *HTTPClient, body: []const u8, body_out_str: *MutableSt
}
pub fn sendHTTP(this: *HTTPClient, body: []const u8, body_out_str: *MutableString) !picohttp.Response {
- var client = try this.connect();
+ this.tcp_client = try this.connect();
defer {
- std.os.closeSocket(client.socket.fd);
+ std.os.closeSocket(this.tcp_client.socket.fd);
}
var request = buildRequest(this, body.len);
if (this.verbose) {
Output.prettyErrorln("{s}", .{request});
}
- var client_writer = client.writer(SOCKET_FLAGS);
+ var client_writer = this.tcp_client.writer(SOCKET_FLAGS);
{
var client_writer_buffered = std.io.bufferedWriter(client_writer);
var client_writer_buffered_writer = client_writer_buffered.writer();
@@ -231,8 +253,56 @@ pub fn sendHTTP(this: *HTTPClient, body: []const u8, body_out_str: *MutableStrin
try client_writer.writeAll(body);
}
- var client_reader = client.reader(SOCKET_FLAGS);
+ var client_reader = this.tcp_client.reader(SOCKET_FLAGS);
+ return this.processResponse(
+ false,
+ @TypeOf(client_reader),
+ client_reader,
+ body_out_str,
+ );
+}
+
+const ZlibPool = struct {
+ lock: Lock = Lock.init(),
+ items: std.ArrayList(*MutableString),
+ allocator: *std.mem.Allocator,
+ pub var instance: ZlibPool = undefined;
+ pub var loaded: bool = false;
+
+ pub fn init(allocator: *std.mem.Allocator) ZlibPool {
+ return ZlibPool{
+ .allocator = allocator,
+ .items = std.ArrayList(*MutableString).init(allocator),
+ };
+ }
+
+ pub fn get(this: *ZlibPool) !*MutableString {
+ this.lock.lock();
+ defer this.lock.unlock();
+ switch (this.items.items.len) {
+ 0 => {
+ var mutable = try this.allocator.create(MutableString);
+ mutable.* = try MutableString.init(this.allocator, 0);
+ return mutable;
+ },
+ else => {
+ return this.items.pop();
+ },
+ }
+
+ return item;
+ }
+
+ pub fn put(this: *ZlibPool, mutable: *MutableString) !void {
+ this.lock.lock();
+ defer this.lock.unlock();
+ mutable.reset();
+ try this.items.append(mutable);
+ }
+};
+
+pub fn processResponse(this: *HTTPClient, comptime is_https: bool, comptime Client: type, client: Client, body_out_str: *MutableString) !picohttp.Response {
var response: picohttp.Response = undefined;
{
@@ -241,7 +311,10 @@ pub fn sendHTTP(this: *HTTPClient, body: []const u8, body_out_str: *MutableStrin
var response_length: usize = 0;
restart: while (req_buf_read != 0) {
- req_buf_read = try client_reader.read(&http_req_buf);
+ req_buf_read = try client.read(if (comptime is_https)
+ &http_req_buf
+ else
+ http_req_buf[req_buf_len..]);
req_buf_len += req_buf_read;
var request_buffer = http_req_buf[0..req_buf_len];
@@ -262,17 +335,27 @@ pub fn sendHTTP(this: *HTTPClient, body: []const u8, body_out_str: *MutableStrin
body_out_str.reset();
var content_length: u32 = 0;
+ var encoding = Encoding.identity;
+
for (response.headers) |header| {
+ if (this.verbose) {
+ Output.prettyErrorln("Response: {s}", .{response});
+ }
+
switch (hashHeaderName(header.name)) {
content_length_header_hash => {
content_length = std.fmt.parseInt(u32, header.value, 10) catch 0;
- // Always write a sentinel
- try body_out_str.inflate(content_length + 1);
+ try body_out_str.inflate(content_length);
body_out_str.list.expandToCapacity();
- body_out_str.list.items[content_length] = 0;
},
content_encoding_hash => {
- return error.UnsupportedEncoding;
+ if (strings.eqlComptime(header.value, "gzip")) {
+ encoding = Encoding.gzip;
+ } else if (strings.eqlComptime(header.value, "deflate")) {
+ encoding = Encoding.deflate;
+ } else if (!strings.eqlComptime(header.value, "identity")) {
+ return error.UnsupportedEncoding;
+ }
},
else => {},
}
@@ -283,22 +366,62 @@ pub fn sendHTTP(this: *HTTPClient, body: []const u8, body_out_str: *MutableStrin
var remainder = http_req_buf[@intCast(u32, response.bytes_read)..];
remainder = remainder[0..std.math.min(remainder.len, content_length)];
+ const Zlib = @import("./zlib.zig");
+
+ var buffer: *MutableString = body_out_str;
+
+ switch (encoding) {
+ Encoding.gzip, Encoding.deflate => {
+ if (!ZlibPool.loaded) {
+ ZlibPool.instance = ZlibPool.init(default_allocator);
+ ZlibPool.loaded = true;
+ }
+
+ buffer = try ZlibPool.instance.get();
+ if (buffer.list.capacity < remaining_content_length) {
+ try buffer.list.ensureUnusedCapacity(buffer.allocator, remaining_content_length);
+ }
+ buffer.list.items = buffer.list.items.ptr[0..remaining_content_length];
+ },
+ else => {},
+ }
+
var body_size: usize = 0;
- if (remainder.len > 0) {
- std.mem.copy(u8, body_out_str.list.items, remainder);
- body_size = @intCast(u32, remainder.len);
- remaining_content_length -= @intCast(u32, remainder.len);
+ if (comptime !is_https) {
+ if (remainder.len > 0) {
+ std.mem.copy(u8, buffer.list.items, remainder);
+ body_size = @intCast(u32, remainder.len);
+ remaining_content_length -= @intCast(u32, remainder.len);
+ }
}
while (remaining_content_length > 0) {
- const size = @intCast(u32, try client.read(body_out_str.list.items[body_size..], SOCKET_FLAGS));
+ const size = @intCast(u32, try client.read(
+ buffer.list.items[body_size..],
+ ));
if (size == 0) break;
body_size += size;
remaining_content_length -= size;
}
- body_out_str.list.items.len = body_size;
+ buffer.list.shrinkRetainingCapacity(body_size);
+
+ switch (encoding) {
+ Encoding.gzip, Encoding.deflate => {
+ body_out_str.list.expandToCapacity();
+ defer ZlibPool.instance.put(buffer) catch unreachable;
+ var reader = try Zlib.ZlibReaderArrayList.init(buffer.list.items, &body_out_str.list, default_allocator);
+ reader.readAll() catch |err| {
+ if (reader.errorMessage()) |msg| {
+ Output.prettyErrorln("<r><red>Zlib error<r>: <b>{s}<r>", .{msg});
+ Output.flush();
+ }
+ return err;
+ };
+ },
+ else => {},
+ }
}
return response;
@@ -352,78 +475,75 @@ pub fn sendHTTPS(this: *HTTPClient, body_str: []const u8, body_out_str: *Mutable
try client_writer.writeAll(body);
}
- var client_reader = client.reader();
- var req_buf_len = try client_reader.readAll(&http_req_buf);
- var request_buffer = http_req_buf[0..req_buf_len];
- var response: picohttp.Response = undefined;
+ var reader = client.reader();
- {
- var response_length: usize = 0;
- restart: while (true) {
- response = picohttp.Response.parseParts(request_buffer, &response_headers_buf, &response_length) catch |err| {
- switch (err) {
- error.ShortRead => {
- continue :restart;
- },
- else => {
- return err;
- },
- }
- };
- break :restart;
- }
- }
+ return try this.processResponse(true, @TypeOf(&reader), &reader, body_out_str);
+}
- body_out_str.reset();
- var content_length: u32 = 0;
- for (response.headers) |header| {
- if (this.verbose) {
- Output.prettyErrorln("Response: {s}", .{response});
- }
+// zig test src/http_client.zig --test-filter "sendHTTP - only" -lc -lc++ /Users/jarred/Code/bun/src/deps/zlib/libz.a /Users/jarred/Code/bun/src/deps/picohttpparser.o --cache-dir /Users/jarred/Code/bun/zig-cache --global-cache-dir /Users/jarred/.cache/zig --name bun --pkg-begin clap /Users/jarred/Code/bun/src/deps/zig-clap/clap.zig --pkg-end --pkg-begin picohttp /Users/jarred/Code/bun/src/deps/picohttp.zig --pkg-end --pkg-begin iguanaTLS /Users/jarred/Code/bun/src/deps/iguanaTLS/src/main.zig --pkg-end -I /Users/jarred/Code/bun/src/deps -I /Users/jarred/Code/bun/src/deps/mimalloc -I /usr/local/opt/icu4c/include -L src/deps/mimalloc -L /usr/local/opt/icu4c/lib --main-pkg-path /Users/jarred/Code/bun --enable-cache -femit-bin=zig-out/bin/test --test-no-exec
+test "sendHTTP - only" {
+ Output.initTest();
+ defer Output.flush();
- switch (hashHeaderName(header.name)) {
- content_length_header_hash => {
- content_length = std.fmt.parseInt(u32, header.value, 10) catch 0;
- try body_out_str.inflate(content_length);
- body_out_str.list.expandToCapacity();
- },
- content_encoding_hash => {
- return error.UnsupportedEncoding;
- },
- else => {},
- }
- }
+ var headers = try std.heap.c_allocator.create(Headers);
+ headers.* = Headers{
+ .entries = @TypeOf(headers.entries){},
+ .buf = @TypeOf(headers.buf){},
+ .used = 0,
+ .allocator = std.heap.c_allocator,
+ };
- if (content_length > 0) {
- var remaining_content_length = content_length;
- var remainder = http_req_buf[@intCast(u32, response.bytes_read)..];
- remainder = remainder[0..std.math.min(remainder.len, content_length)];
+ // headers.appendHeader("X-What", "ok", true, true, false);
+ headers.appendHeader("Accept-Encoding", "identity", true, true, false);
- var body_size: usize = 0;
- if (remainder.len > 0) {
- std.mem.copy(u8, body_out_str.list.items, remainder);
- body_size = @intCast(u32, remainder.len);
- remaining_content_length -= @intCast(u32, remainder.len);
- }
+ var client = HTTPClient.init(
+ std.heap.c_allocator,
+ .GET,
+ URL.parse("http://example.com/"),
+ headers.entries,
+ headers.buf.items,
+ );
+ var body_out_str = try MutableString.init(std.heap.c_allocator, 0);
+ var response = try client.sendHTTP("", &body_out_str);
+ try std.testing.expectEqual(response.status_code, 200);
+ try std.testing.expectEqual(body_out_str.list.items.len, 1256);
+ try std.testing.expectEqualStrings(body_out_str.list.items, @embedFile("fixtures_example.com.html"));
+}
- while (remaining_content_length > 0) {
- const size = @intCast(u32, try client.read(
- body_out_str.list.items[body_size..],
- ));
- if (size == 0) break;
+// zig test src/http_client.zig --test-filter "sendHTTP - gzip" -lc -lc++ /Users/jarred/Code/bun/src/deps/zlib/libz.a /Users/jarred/Code/bun/src/deps/picohttpparser.o --cache-dir /Users/jarred/Code/bun/zig-cache --global-cache-dir /Users/jarred/.cache/zig --name bun --pkg-begin clap /Users/jarred/Code/bun/src/deps/zig-clap/clap.zig --pkg-end --pkg-begin picohttp /Users/jarred/Code/bun/src/deps/picohttp.zig --pkg-end --pkg-begin iguanaTLS /Users/jarred/Code/bun/src/deps/iguanaTLS/src/main.zig --pkg-end -I /Users/jarred/Code/bun/src/deps -I /Users/jarred/Code/bun/src/deps/mimalloc -I /usr/local/opt/icu4c/include -L src/deps/mimalloc -L /usr/local/opt/icu4c/lib --main-pkg-path /Users/jarred/Code/bun --enable-cache -femit-bin=zig-out/bin/test --test-no-exec
+test "sendHTTP - gzip" {
+ Output.initTest();
+ defer Output.flush();
- body_size += size;
- remaining_content_length -= size;
- }
+ var headers = try std.heap.c_allocator.create(Headers);
+ headers.* = Headers{
+ .entries = @TypeOf(headers.entries){},
+ .buf = @TypeOf(headers.buf){},
+ .used = 0,
+ .allocator = std.heap.c_allocator,
+ };
- body_out_str.list.shrinkRetainingCapacity(body_size);
- }
+ // headers.appendHeader("X-What", "ok", true, true, false);
+ headers.appendHeader("Accept-Encoding", "gzip", true, true, false);
- return response;
+ var client = HTTPClient.init(
+ std.heap.c_allocator,
+ .GET,
+ URL.parse("http://example.com/"),
+ headers.entries,
+ headers.buf.items,
+ );
+ var body_out_str = try MutableString.init(std.heap.c_allocator, 0);
+ var response = try client.sendHTTP("", &body_out_str);
+ try std.testing.expectEqual(response.status_code, 200);
+ try std.testing.expectEqualStrings(body_out_str.list.items, @embedFile("fixtures_example.com.html"));
}
-// zig test src/http_client.zig --test-filter "sendHTTP" -lc -lc++ /Users/jarred/Code/bun/src/deps/picohttpparser.o --cache-dir /Users/jarred/Code/bun/zig-cache --global-cache-dir /Users/jarred/.cache/zig --name bun --pkg-begin clap /Users/jarred/Code/bun/src/deps/zig-clap/clap.zig --pkg-end --pkg-begin picohttp /Users/jarred/Code/bun/src/deps/picohttp.zig --pkg-end --pkg-begin iguanaTLS /Users/jarred/Code/bun/src/deps/iguanaTLS/src/main.zig --pkg-end -I /Users/jarred/Code/bun/src/deps -I /Users/jarred/Code/bun/src/deps/mimalloc -I /usr/local/opt/icu4c/include -L src/deps/mimalloc -L /usr/local/opt/icu4c/lib --main-pkg-path /Users/jarred/Code/bun --enable-cache
-test "sendHTTP" {
+// zig test src/http_client.zig --test-filter "sendHTTPS - identity" -lc -lc++ /Users/jarred/Code/bun/src/deps/zlib/libz.a /Users/jarred/Code/bun/src/deps/picohttpparser.o --cache-dir /Users/jarred/Code/bun/zig-cache --global-cache-dir /Users/jarred/.cache/zig --name bun --pkg-begin clap /Users/jarred/Code/bun/src/deps/zig-clap/clap.zig --pkg-end --pkg-begin picohttp /Users/jarred/Code/bun/src/deps/picohttp.zig --pkg-end --pkg-begin iguanaTLS /Users/jarred/Code/bun/src/deps/iguanaTLS/src/main.zig --pkg-end -I /Users/jarred/Code/bun/src/deps -I /Users/jarred/Code/bun/src/deps/mimalloc -I /usr/local/opt/icu4c/include -L src/deps/mimalloc -L /usr/local/opt/icu4c/lib --main-pkg-path /Users/jarred/Code/bun --enable-cache -femit-bin=zig-out/bin/test --test-no-exec
+test "sendHTTPS - identity" {
+ Output.initTest();
+ defer Output.flush();
+
var headers = try std.heap.c_allocator.create(Headers);
headers.* = Headers{
.entries = @TypeOf(headers.entries){},
@@ -433,22 +553,26 @@ test "sendHTTP" {
};
headers.appendHeader("X-What", "ok", true, true, false);
+ headers.appendHeader("Accept-Encoding", "identity", true, true, false);
var client = HTTPClient.init(
std.heap.c_allocator,
.GET,
- URL.parse("http://example.com/"),
+ URL.parse("https://example.com/"),
headers.entries,
headers.buf.items,
);
var body_out_str = try MutableString.init(std.heap.c_allocator, 0);
- var response = try client.sendHTTP("", &body_out_str);
+ var response = try client.sendHTTPS("", &body_out_str);
try std.testing.expectEqual(response.status_code, 200);
- try std.testing.expectEqual(body_out_str.list.items.len, 1256);
+ try std.testing.expectEqualStrings(body_out_str.list.items, @embedFile("fixtures_example.com.html"));
}
-// zig test src/http_client.zig --test-filter "sendHTTPS" -lc -lc++ /Users/jarred/Code/bun/src/deps/picohttpparser.o --cache-dir /Users/jarred/Code/bun/zig-cache --global-cache-dir /Users/jarred/.cache/zig --name bun --pkg-begin clap /Users/jarred/Code/bun/src/deps/zig-clap/clap.zig --pkg-end --pkg-begin picohttp /Users/jarred/Code/bun/src/deps/picohttp.zig --pkg-end --pkg-begin iguanaTLS /Users/jarred/Code/bun/src/deps/iguanaTLS/src/main.zig --pkg-end -I /Users/jarred/Code/bun/src/deps -I /Users/jarred/Code/bun/src/deps/mimalloc -I /usr/local/opt/icu4c/include -L src/deps/mimalloc -L /usr/local/opt/icu4c/lib --main-pkg-path /Users/jarred/Code/bun --enable-cache
-test "sendHTTPS" {
+// zig test src/http_client.zig --test-filter "sendHTTPS - gzip" -lc -lc++ /Users/jarred/Code/bun/src/deps/zlib/libz.a /Users/jarred/Code/bun/src/deps/picohttpparser.o --cache-dir /Users/jarred/Code/bun/zig-cache --global-cache-dir /Users/jarred/.cache/zig --name bun --pkg-begin clap /Users/jarred/Code/bun/src/deps/zig-clap/clap.zig --pkg-end --pkg-begin picohttp /Users/jarred/Code/bun/src/deps/picohttp.zig --pkg-end --pkg-begin iguanaTLS /Users/jarred/Code/bun/src/deps/iguanaTLS/src/main.zig --pkg-end -I /Users/jarred/Code/bun/src/deps -I /Users/jarred/Code/bun/src/deps/mimalloc -I /usr/local/opt/icu4c/include -L src/deps/mimalloc -L /usr/local/opt/icu4c/lib --main-pkg-path /Users/jarred/Code/bun --enable-cache -femit-bin=zig-out/bin/test --test-no-exec
+test "sendHTTPS - gzip" {
+ Output.initTest();
+ defer Output.flush();
+
var headers = try std.heap.c_allocator.create(Headers);
headers.* = Headers{
.entries = @TypeOf(headers.entries){},
@@ -457,17 +581,19 @@ test "sendHTTPS" {
.allocator = std.heap.c_allocator,
};
- headers.appendHeader("X-What", "ok", true, true, false);
+ headers.appendHeader("Accept-Encoding", "gzip", false, false, false);
var client = HTTPClient.init(
std.heap.c_allocator,
.GET,
- URL.parse("https://hookb.in/aBnOOWN677UXQ9kkQ2g3"),
+ URL.parse("https://example.com/"),
headers.entries,
headers.buf.items,
);
var body_out_str = try MutableString.init(std.heap.c_allocator, 0);
var response = try client.sendHTTPS("", &body_out_str);
try std.testing.expectEqual(response.status_code, 200);
- try std.testing.expectEqual(body_out_str.list.items.len, 1256);
+ try std.testing.expectEqualStrings(body_out_str.list.items, @embedFile("fixtures_example.com.html"));
}
+
+// zig test src/http_client.zig --test-filter "sendHTTP" -lc -lc++ /Users/jarred/Code/bun/src/deps/zlib/libz.a /Users/jarred/Code/bun/src/deps/picohttpparser.o --cache-dir /Users/jarred/Code/bun/zig-cache --global-cache-dir /Users/jarred/.cache/zig --name bun --pkg-begin clap /Users/jarred/Code/bun/src/deps/zig-clap/clap.zig --pkg-end --pkg-begin picohttp /Users/jarred/Code/bun/src/deps/picohttp.zig --pkg-end --pkg-begin iguanaTLS /Users/jarred/Code/bun/src/deps/iguanaTLS/src/main.zig --pkg-end -I /Users/jarred/Code/bun/src/deps -I /Users/jarred/Code/bun/src/deps/mimalloc -I /usr/local/opt/icu4c/include -L src/deps/mimalloc -L /usr/local/opt/icu4c/lib --main-pkg-path /Users/jarred/Code/bun --enable-cache -femit-bin=zig-out/bin/test