Commit 1afeada2d9
Changed files (3)
lib
std
test
standalone
lib/std/http/Client.zig
@@ -18,6 +18,7 @@ pub const connection_pool_size = std.options.http_connection_pool_size;
allocator: Allocator,
ca_bundle: std.crypto.Certificate.Bundle = .{},
ca_bundle_mutex: std.Thread.Mutex = .{},
+
/// When this is `true`, the next time this client performs an HTTPS request,
/// it will first rescan the system for root certificates.
next_https_rescan_certs: bool = true,
@@ -25,7 +26,11 @@ next_https_rescan_certs: bool = true,
/// The pool of connections that can be reused (and currently in use).
connection_pool: ConnectionPool = .{},
-proxy: ?HttpProxy = null,
+/// This is the proxy that will handle http:// connections. It *must not* be modified when the client has any active connections.
+http_proxy: ?ProxyInformation = null,
+
+/// This is the proxy that will handle https:// connections. It *must not* be modified when the client has any active connections.
+https_proxy: ?ProxyInformation = null,
/// A set of linked lists of connections that can be reused.
pub const ConnectionPool = struct {
@@ -33,7 +38,7 @@ pub const ConnectionPool = struct {
pub const Criteria = struct {
host: []const u8,
port: u16,
- is_tls: bool,
+ protocol: Connection.Protocol,
};
const Queue = std.DoublyLinkedList(Connection);
@@ -55,9 +60,9 @@ pub const ConnectionPool = struct {
var next = pool.free.last;
while (next) |node| : (next = node.prev) {
- if ((node.data.protocol == .tls) != criteria.is_tls) continue;
+ if (node.data.protocol != criteria.protocol) continue;
if (node.data.port != criteria.port) continue;
- if (!mem.eql(u8, node.data.host, criteria.host)) continue;
+ if (!std.ascii.eqlIgnoreCase(node.data.host, criteria.host)) continue;
pool.acquireUnsafe(node);
return node;
@@ -84,23 +89,23 @@ pub const ConnectionPool = struct {
/// Tries to release a connection back to the connection pool. This function is threadsafe.
/// If the connection is marked as closing, it will be closed instead.
- pub fn release(pool: *ConnectionPool, client: *Client, node: *Node) void {
+ pub fn release(pool: *ConnectionPool, allocator: Allocator, node: *Node) void {
pool.mutex.lock();
defer pool.mutex.unlock();
pool.used.remove(node);
- if (node.data.closing) {
- node.data.deinit(client);
- return client.allocator.destroy(node);
+ if (node.data.closing or pool.free_size == 0) {
+ node.data.close(allocator);
+ return allocator.destroy(node);
}
if (pool.free_len >= pool.free_size) {
const popped = pool.free.popFirst() orelse unreachable;
pool.free_len -= 1;
- popped.data.deinit(client);
- client.allocator.destroy(popped);
+ popped.data.close(allocator);
+ allocator.destroy(popped);
}
if (node.data.proxied) {
@@ -128,7 +133,7 @@ pub const ConnectionPool = struct {
defer client.allocator.destroy(node);
next = node.next;
- node.data.deinit(client);
+ node.data.close(client.allocator);
}
next = pool.used.first;
@@ -136,7 +141,7 @@ pub const ConnectionPool = struct {
defer client.allocator.destroy(node);
next = node.next;
- node.data.deinit(client);
+ node.data.close(client.allocator);
}
pool.* = undefined;
@@ -283,19 +288,15 @@ pub const Connection = struct {
return Writer{ .context = conn };
}
- pub fn close(conn: *Connection, client: *const Client) void {
+ pub fn close(conn: *Connection, allocator: Allocator) void {
if (conn.protocol == .tls) {
// try to cleanly close the TLS connection, for any server that cares.
_ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};
- client.allocator.destroy(conn.tls_client);
+ allocator.destroy(conn.tls_client);
}
conn.stream.close();
- }
-
- pub fn deinit(conn: *Connection, client: *const Client) void {
- conn.close(client);
- client.allocator.free(conn.host);
+ allocator.free(conn.host);
}
};
@@ -490,7 +491,7 @@ pub const Request = struct {
// If the response wasn't fully read, then we need to close the connection.
connection.data.closing = true;
}
- req.client.connection_pool.release(req.client, connection);
+ req.client.connection_pool.release(req.client.allocator, connection);
}
req.arena.deinit();
@@ -509,7 +510,7 @@ pub const Request = struct {
.zstd => |*zstd| zstd.deinit(),
}
- req.client.connection_pool.release(req.client, req.connection.?);
+ req.client.connection_pool.release(req.client.allocator, req.connection.?);
req.connection = null;
const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
@@ -554,24 +555,16 @@ pub const Request = struct {
try w.writeByte(' ');
if (req.method == .CONNECT) {
- try w.writeAll(req.uri.host.?);
- try w.writeByte(':');
- try w.print("{}", .{req.uri.port.?});
+ try req.uri.writeToStream(.{ .authority = true }, w);
} else {
- if (req.connection.?.data.proxied) {
- // proxied connections require the full uri
- if (options.raw_uri) {
- try w.print("{+/r}", .{req.uri});
- } else {
- try w.print("{+/}", .{req.uri});
- }
- } else {
- if (options.raw_uri) {
- try w.print("{/r}", .{req.uri});
- } else {
- try w.print("{/}", .{req.uri});
- }
- }
+ try req.uri.writeToStream(.{
+ .scheme = req.connection.?.data.proxied,
+ .authentication = req.connection.?.data.proxied,
+ .authority = req.connection.?.data.proxied,
+ .path = true,
+ .query = true,
+ .raw = options.raw_uri,
+ }, w);
}
try w.writeByte(' ');
try w.writeAll(@tagName(req.version));
@@ -579,7 +572,7 @@ pub const Request = struct {
if (!req.headers.contains("host")) {
try w.writeAll("Host: ");
- try w.writeAll(req.uri.host.?);
+ try req.uri.writeToStream(.{ .authority = true }, w);
try w.writeAll("\r\n");
}
@@ -636,6 +629,24 @@ pub const Request = struct {
try w.writeAll("\r\n");
}
+ if (req.connection.?.data.proxied) {
+ const proxy_headers: ?http.Headers = switch (req.connection.?.data.protocol) {
+ .plain => if (req.client.http_proxy) |proxy| proxy.headers else null,
+ .tls => if (req.client.https_proxy) |proxy| proxy.headers else null,
+ };
+
+ if (proxy_headers) |headers| {
+ for (headers.list.items) |entry| {
+ if (entry.value.len == 0) continue;
+
+ try w.writeAll(entry.name);
+ try w.writeAll(": ");
+ try w.writeAll(entry.value);
+ try w.writeAll("\r\n");
+ }
+ }
+ }
+
try w.writeAll("\r\n");
try buffered.flush();
@@ -893,18 +904,15 @@ pub const Request = struct {
}
};
-pub const HttpProxy = struct {
- pub const ProxyAuthentication = union(enum) {
- basic: []const u8,
- custom: []const u8,
- };
+pub const ProxyInformation = struct {
+ allocator: Allocator,
+ headers: http.Headers,
protocol: Connection.Protocol,
host: []const u8,
- port: ?u16 = null,
+ port: u16,
- /// The value for the Proxy-Authorization header.
- auth: ?ProxyAuthentication = null,
+ supports_connect: bool = true,
};
/// Release all associated resources with the client.
@@ -912,19 +920,115 @@ pub const HttpProxy = struct {
pub fn deinit(client: *Client) void {
client.connection_pool.deinit(client);
+ if (client.http_proxy) |*proxy| {
+ proxy.allocator.free(proxy.host);
+ proxy.headers.deinit();
+ }
+
+ if (client.https_proxy) |*proxy| {
+ proxy.allocator.free(proxy.host);
+ proxy.headers.deinit();
+ }
+
client.ca_bundle.deinit(client.allocator);
client.* = undefined;
}
-pub const ConnectUnproxiedError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };
+/// Uses the *_proxy environment variable to set any unset proxies for the client.
+/// This function *must not* be called when the client has any active connections.
+pub fn loadDefaultProxies(client: *Client) !void {
+ if (client.http_proxy == null) http: {
+ const content: []const u8 = if (std.process.hasEnvVarConstant("http_proxy"))
+ try std.process.getEnvVarOwned(client.allocator, "http_proxy")
+ else if (std.process.hasEnvVarConstant("HTTP_PROXY"))
+ try std.process.getEnvVarOwned(client.allocator, "HTTP_PROXY")
+ else if (std.process.hasEnvVarConstant("all_proxy"))
+ try std.process.getEnvVarOwned(client.allocator, "all_proxy")
+ else if (std.process.hasEnvVarConstant("ALL_PROXY"))
+ try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
+ else
+ break :http;
+ defer client.allocator.free(content);
+
+ const uri = try Uri.parse(content);
+
+ const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
+ client.http_proxy = .{
+ .allocator = client.allocator,
+ .headers = .{ .allocator = client.allocator },
+
+ .protocol = protocol,
+ .host = if (uri.host) |host| try client.allocator.dupe(u8, host) else return error.UriMissingHost,
+ .port = uri.port orelse switch (protocol) {
+ .plain => 80,
+ .tls => 443,
+ },
+ };
+
+ if (uri.user != null and uri.password != null) {
+ const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? });
+ defer client.allocator.free(unencoded);
+
+ const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len));
+ defer client.allocator.free(buffer);
+
+ const result = std.base64.standard.Encoder.encode(buffer, unencoded);
+
+ try client.http_proxy.?.headers.append("proxy-authorization", result);
+ }
+ }
+
+ if (client.https_proxy == null) https: {
+ const content: []const u8 = if (std.process.hasEnvVarConstant("https_proxy"))
+ try std.process.getEnvVarOwned(client.allocator, "https_proxy")
+ else if (std.process.hasEnvVarConstant("HTTPS_PROXY"))
+ try std.process.getEnvVarOwned(client.allocator, "HTTPS_PROXY")
+ else if (std.process.hasEnvVarConstant("all_proxy"))
+ try std.process.getEnvVarOwned(client.allocator, "all_proxy")
+ else if (std.process.hasEnvVarConstant("ALL_PROXY"))
+ try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
+ else
+ break :https;
+ defer client.allocator.free(content);
+
+ const uri = try Uri.parse(content);
+
+ const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
+ client.http_proxy = .{
+ .allocator = client.allocator,
+ .headers = .{ .allocator = client.allocator },
+
+ .protocol = protocol,
+ .host = if (uri.host) |host| try client.allocator.dupe(u8, host) else return error.UriMissingHost,
+ .port = uri.port orelse switch (protocol) {
+ .plain => 80,
+ .tls => 443,
+ },
+ };
+
+ if (uri.user != null and uri.password != null) {
+ const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? });
+ defer client.allocator.free(unencoded);
+
+ const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len));
+ defer client.allocator.free(buffer);
+
+ const result = std.base64.standard.Encoder.encode(buffer, unencoded);
+
+ try client.https_proxy.?.headers.append("proxy-authorization", result);
+ }
+ }
+}
+
+pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };
/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
/// This function is threadsafe.
-pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectUnproxiedError!*ConnectionPool.Node {
+pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*ConnectionPool.Node {
if (client.connection_pool.findConnection(.{
.host = host,
.port = port,
- .is_tls = protocol == .tls,
+ .protocol = protocol,
})) |node|
return node;
@@ -948,8 +1052,8 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol:
conn.data = .{
.stream = stream,
.tls_client = undefined,
- .protocol = protocol,
+ .protocol = protocol,
.host = try client.allocator.dupe(u8, host),
.port = port,
};
@@ -981,7 +1085,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
if (client.connection_pool.findConnection(.{
.host = path,
.port = 0,
- .is_tls = false,
+ .protocol = .plain,
})) |node|
return node;
@@ -1007,34 +1111,120 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
return conn;
}
-// Prevents a dependency loop in request()
-const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused };
-pub const ConnectError = ConnectErrorPartial || RequestError;
+pub fn connectTunnel(
+ client: *Client,
+ proxy: *ProxyInformation,
+ tunnel_host: []const u8,
+ tunnel_port: u16,
+) !*ConnectionPool.Node {
+ if (!proxy.supports_connect) return error.TunnelNotSupported;
-pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
if (client.connection_pool.findConnection(.{
- .host = host,
- .port = port,
- .is_tls = protocol == .tls,
+ .host = tunnel_host,
+ .port = tunnel_port,
+ .protocol = proxy.protocol,
})) |node|
return node;
- if (client.proxy) |proxy| {
- const proxy_port: u16 = proxy.port orelse switch (proxy.protocol) {
- .plain => 80,
- .tls => 443,
+ var maybe_valid = false;
+ _ = tunnel: {
+ const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
+ errdefer {
+ conn.data.closing = true;
+ client.connection_pool.release(client.allocator, conn);
+ }
+
+ const uri = Uri{
+ .scheme = "http",
+ .user = null,
+ .password = null,
+ .host = tunnel_host,
+ .port = tunnel_port,
+ .path = "",
+ .query = null,
+ .fragment = null,
};
- const conn = try client.connectUnproxied(proxy.host, proxy_port, proxy.protocol);
- conn.data.proxied = true;
+ // we can use a small buffer here because a CONNECT response should be very small
+ var buffer: [8096]u8 = undefined;
+
+ var req = client.request(.CONNECT, uri, proxy.headers, .{
+ .handle_redirects = false,
+ .connection = conn,
+ .header_strategy = .{ .static = buffer[0..] },
+ }) catch |err| {
+ std.log.debug("err {}", .{err});
+ break :tunnel err;
+ };
+ defer req.deinit();
+
+ req.start(.{ .raw_uri = true }) catch |err| break :tunnel err;
+ req.wait() catch |err| break :tunnel err;
+
+ if (req.response.status.class() == .server_error) {
+ maybe_valid = true;
+ break :tunnel error.ServerError;
+ }
+
+ if (req.response.status != .ok) break :tunnel error.ConnectionRefused;
+ // this connection is now a tunnel, so we can't use it for anything else, it will only be released when the client is de-initialized.
+ req.connection = null;
+
+ client.allocator.free(conn.data.host);
+ conn.data.host = try client.allocator.dupe(u8, tunnel_host);
+ errdefer client.allocator.free(conn.data.host);
+
+ conn.data.port = tunnel_port;
+ conn.data.closing = false;
+
+ return conn;
+ } catch {
+ // something went wrong with the tunnel
+ proxy.supports_connect = maybe_valid;
+ return error.TunnelNotSupported;
+ };
+}
+
+// Prevents a dependency loop in request()
+const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };
+pub const ConnectError = ConnectErrorPartial || RequestError;
+
+pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
+ // pointer required so that `supports_connect` can be updated if a CONNECT fails
+ const potential_proxy: ?*ProxyInformation = switch (protocol) {
+ .plain => if (client.http_proxy) |*proxy_info| proxy_info else null,
+ .tls => if (client.https_proxy) |*proxy_info| proxy_info else null,
+ };
+
+ if (potential_proxy) |proxy| {
+ // don't attempt to proxy the proxy thru itself.
+ if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) {
+ return client.connectTcp(host, port, protocol);
+ }
+
+ _ = if (proxy.supports_connect) tunnel: {
+ return connectTunnel(client, proxy, host, port) catch |err| switch (err) {
+ error.TunnelNotSupported => break :tunnel,
+ else => |e| return e,
+ };
+ };
+
+ // fall back to using the proxy as a normal http proxy
+ const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
+ errdefer {
+ conn.data.closing = true;
+ client.connection_pool.release(conn);
+ }
+
+ conn.data.proxied = true;
return conn;
- } else {
- return client.connectUnproxied(host, port, protocol);
}
+
+ return client.connectTcp(host, port, protocol);
}
-pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{
+pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{
UnsupportedUrlScheme,
UriMissingHost,
lib/std/Uri.zig
@@ -208,24 +208,45 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
return uri;
}
-pub fn format(
+pub const WriteToStreamOptions = struct {
+ /// When true, include the scheme part of the URI.
+ scheme: bool = false,
+
+ /// When true, include the user and password part of the URI. Ignored if `authority` is false.
+ authentication: bool = false,
+
+ /// When true, include the authority part of the URI.
+ authority: bool = false,
+
+ /// When true, include the path part of the URI.
+ path: bool = false,
+
+ /// When true, include the query part of the URI. Ignored when `path` is false.
+ query: bool = false,
+
+ /// When true, include the fragment part of the URI. Ignored when `path` is false.
+ fragment: bool = false,
+
+ /// When true, do not escape any part of the URI.
+ raw: bool = false,
+};
+
+pub fn writeToStream(
uri: Uri,
- comptime fmt: []const u8,
- options: std.fmt.FormatOptions,
+ options: WriteToStreamOptions,
writer: anytype,
) @TypeOf(writer).Error!void {
- _ = options;
-
- const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;
- const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
- const raw_uri = comptime std.mem.indexOf(u8, fmt, "r") != null;
- const needs_fragment = comptime std.mem.indexOf(u8, fmt, "#") != null;
-
- if (needs_absolute) {
+ if (options.scheme) {
try writer.writeAll(uri.scheme);
try writer.writeAll(":");
- if (uri.host) |host| {
+
+ if (options.authority and uri.host != null) {
try writer.writeAll("//");
+ }
+ }
+
+ if (options.authority) {
+ if (options.authentication and uri.host != null) {
if (uri.user) |user| {
try writer.writeAll(user);
if (uri.password) |password| {
@@ -234,7 +255,9 @@ pub fn format(
}
try writer.writeAll("@");
}
+ }
+ if (uri.host) |host| {
try writer.writeAll(host);
if (uri.port) |port| {
@@ -244,39 +267,62 @@ pub fn format(
}
}
- if (needs_path) {
+ if (options.path) {
if (uri.path.len == 0) {
try writer.writeAll("/");
+ } else if (options.raw) {
+ try writer.writeAll(uri.path);
} else {
- if (raw_uri) {
- try writer.writeAll(uri.path);
- } else {
- try Uri.writeEscapedPath(writer, uri.path);
- }
+ try writeEscapedPath(writer, uri.path);
}
- if (uri.query) |q| {
+ if (options.query) if (uri.query) |q| {
try writer.writeAll("?");
- if (raw_uri) {
+ if (options.raw) {
try writer.writeAll(q);
} else {
- try Uri.writeEscapedQuery(writer, q);
+ try writeEscapedQuery(writer, q);
}
- }
+ };
- if (needs_fragment) {
- if (uri.fragment) |f| {
- try writer.writeAll("#");
- if (raw_uri) {
- try writer.writeAll(f);
- } else {
- try Uri.writeEscapedQuery(writer, f);
- }
+ if (options.fragment) if (uri.fragment) |f| {
+ try writer.writeAll("#");
+ if (options.raw) {
+ try writer.writeAll(f);
+ } else {
+ try writeEscapedQuery(writer, f);
}
- }
+ };
}
}
+pub fn format(
+ uri: Uri,
+ comptime fmt: []const u8,
+ options: std.fmt.FormatOptions,
+ writer: anytype,
+) @TypeOf(writer).Error!void {
+ _ = options;
+
+ const scheme = comptime std.mem.indexOf(u8, fmt, ":") != null or fmt.len == 0;
+ const authentication = comptime std.mem.indexOf(u8, fmt, "@") != null or fmt.len == 0;
+ const authority = comptime std.mem.indexOf(u8, fmt, "+") != null or fmt.len == 0;
+ const path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
+ const query = comptime std.mem.indexOf(u8, fmt, "?") != null or fmt.len == 0;
+ const fragment = comptime std.mem.indexOf(u8, fmt, "#") != null or fmt.len == 0;
+ const raw = comptime std.mem.indexOf(u8, fmt, "r") != null or fmt.len == 0;
+
+ return writeToStream(uri, .{
+ .scheme = scheme,
+ .authentication = authentication,
+ .authority = authority,
+ .path = path,
+ .query = query,
+ .fragment = fragment,
+ .raw = raw,
+ }, writer);
+}
+
/// Parses the URI or returns an error.
/// The return value will contain unescaped strings pointing into the
/// original `text`. Each component that is provided, will be non-`null`.
@@ -709,7 +755,7 @@ test "URI query escaping" {
const parsed = try Uri.parse(address);
// format the URI to escape it
- const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{}", .{parsed});
+ const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{/?}", .{parsed});
defer std.testing.allocator.free(formatted_uri);
try std.testing.expectEqualStrings("/?response-content-type=application%2Foctet-stream", formatted_uri);
}
@@ -727,6 +773,6 @@ test "format" {
};
var buf = std.ArrayList(u8).init(std.testing.allocator);
defer buf.deinit();
- try uri.format("+/", .{}, buf.writer());
+ try uri.format(":/?#", .{}, buf.writer());
try std.testing.expectEqualSlices(u8, "file:/foo/bar/baz", buf.items);
}
test/standalone/http.zig
@@ -226,8 +226,11 @@ pub fn main() !void {
const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});
var client = Client{ .allocator = calloc };
+ errdefer client.deinit();
// defer client.deinit(); handled below
+ try client.loadDefaultProxies();
+
{ // read content-length response
var h = http.Headers{ .allocator = calloc };
defer h.deinit();
@@ -251,7 +254,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // read large content-length response
var h = http.Headers{ .allocator = calloc };
@@ -275,7 +278,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // send head request and not read chunked
var h = http.Headers{ .allocator = calloc };
@@ -301,7 +304,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // read chunked response
var h = http.Headers{ .allocator = calloc };
@@ -326,7 +329,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // send head request and not read chunked
var h = http.Headers{ .allocator = calloc };
@@ -352,7 +355,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // check trailing headers
var h = http.Headers{ .allocator = calloc };
@@ -377,7 +380,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // send content-length request
var h = http.Headers{ .allocator = calloc };
@@ -409,7 +412,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // read content-length response with connection close
var h = http.Headers{ .allocator = calloc };
@@ -468,7 +471,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // relative redirect
var h = http.Headers{ .allocator = calloc };
@@ -492,7 +495,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // redirect from root
var h = http.Headers{ .allocator = calloc };
@@ -516,7 +519,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // absolute redirect
var h = http.Headers{ .allocator = calloc };
@@ -540,7 +543,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // too many redirects
var h = http.Headers{ .allocator = calloc };
@@ -562,7 +565,7 @@ pub fn main() !void {
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // check client without segfault by connection error after redirection
var h = http.Headers{ .allocator = calloc };
@@ -579,11 +582,14 @@ pub fn main() !void {
try req.start(.{});
const result = req.wait();
- try testing.expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
+ // a proxy without an upstream is likely to return a 5xx status.
+ if (client.http_proxy == null) {
+ try testing.expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
+ }
}
// connection has been kept alive
- try testing.expect(client.connection_pool.free_len == 1);
+ try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // Client.fetch()
var h = http.Headers{ .allocator = calloc };