From 008f7845b4f61b8b5410b71df15d69f827360cf7 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 25 Aug 2026 23:06:46 +1200 Subject: [PATCH 1/2] Add coverage for closing persistent proxy clients --- test/async/http/faraday/clients.rb | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/async/http/faraday/clients.rb b/test/async/http/faraday/clients.rb index 1adc526..af72269 100644 --- a/test/async/http/faraday/clients.rb +++ b/test/async/http/faraday/clients.rb @@ -6,6 +6,72 @@ require "async/http/faraday/clients" require "async/http/middleware/location_redirector" +require "sus/fixtures/async/http/server_context" + +require "socket" + +class ConnectProxy + def initialize + @server = TCPServer.new("127.0.0.1", 0) + @workers = [] + @thread = Thread.new do + loop do + client = @server.accept + @workers << Thread.new{tunnel(client)} + end + rescue IOError, Errno::EBADF + # The server was closed. + end + end + + def endpoint + Async::HTTP::Endpoint.parse("http://127.0.0.1:#{@server.local_address.ip_port}") + end + + def close + @server.close + @thread.join + @workers.each(&:join) + end + + private + + def tunnel(client) + request = client.gets + return unless request&.start_with?("CONNECT ") + + authority = request.split(" ", 3)[1] + host, port = authority.split(":", 2) + + while (line = client.gets) && line != "\r\n" + end + + upstream = TCPSocket.new(host, port) + client.write("HTTP/1.1 200 Connection established\r\n\r\n") + + copy(client, upstream) + ensure + client&.close + upstream&.close + end + + def copy(client, upstream) + pump = lambda do |input, output| + Thread.new do + IO.copy_stream(input, output) + rescue IOError, SystemCallError + # Either side of the tunnel was closed. + ensure + output.close_write rescue nil + end + end + + threads = [pump.call(client, upstream), pump.call(upstream, client)] + + threads.each(&:join) + end +end + describe Async::HTTP::Faraday::PersistentClients do let(:clients) {subject.new} @@ -66,6 +132,31 @@ clients.close end + + with "a CONNECT proxy" do + include Sus::Fixtures::Async::HTTP::ServerContext + + it "closes the tunnel before the proxy client" do + proxy = ConnectProxy.new + endpoint = Async::HTTP::Endpoint.parse(bound_url) + closed = false + + clients.with_proxied_client(proxy.endpoint, endpoint) do |client| + response = client.get("/") + expect(response.read).to be == "Hello World!" + end + + cached_clients = clients.instance_variable_get(:@clients).values + + Async::Task.current.with_timeout(1) do + clients.close + closed = true + end + ensure + cached_clients&.reverse_each(&:close) unless closed + proxy&.close + end + end end end From 8e5730face19e24fea18cef1f387f75c7f446d51 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 25 Aug 2026 23:07:04 +1200 Subject: [PATCH 2/2] Close persistent clients in dependency order --- lib/async/http/faraday/clients.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/http/faraday/clients.rb b/lib/async/http/faraday/clients.rb index b9e5c54..e518b67 100644 --- a/lib/async/http/faraday/clients.rb +++ b/lib/async/http/faraday/clients.rb @@ -86,7 +86,7 @@ def close clients = @clients.values @clients.clear - clients.each(&:close) + clients.reverse_each(&:close) end # Lookup or create a client for the given endpoint.