diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index 6952839e71..0a98c5b641 100644 --- a/lib/std/Uri.zig +++ b/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); } diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 9e475df51b..d97334fe4c 100644 --- a/lib/std/http/Client.zig +++ b/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; - } else { - return client.connectUnproxied(host, port, protocol); - } + } catch { + // something went wrong with the tunnel + proxy.supports_connect = maybe_valid; + return error.TunnelNotSupported; + }; } -pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{ +// 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; + } + + return client.connectTcp(host, port, protocol); +} + +pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{ UnsupportedUrlScheme, UriMissingHost, diff --git a/test/standalone/http.zig b/test/standalone/http.zig index 00fd4397b0..8b538a092f 100644 --- a/test/standalone/http.zig +++ b/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 };