diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index 2e388fe..f2eed91 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -91,6 +91,9 @@ class Client # request to generate the payload dynamically. # @param retry_enabled [Boolean] (true) whether to retry connections after failures. If false, the client # will exit after the first connection failure instead of attempting to reconnect. + # @param http_client_options [Hash] (nil) additional options to pass to + # the HTTP client, such as `socket_factory` or `proxy`. These settings will override + # the socket factory and proxy settings. # @yieldparam [Client] client the new client instance, before opening the connection # def initialize(uri, @@ -105,7 +108,8 @@ def initialize(uri, socket_factory: nil, method: "GET", payload: nil, - retry_enabled: true) + retry_enabled: true, + http_client_options: nil) @uri = URI(uri) @stopped = Concurrent::AtomicBoolean.new(false) @retry_enabled = retry_enabled @@ -116,9 +120,10 @@ def initialize(uri, @method = method.to_s.upcase @payload = payload @logger = logger || default_logger - http_client_options = {} + + base_http_client_options = {} if socket_factory - http_client_options["socket_class"] = socket_factory + base_http_client_options["socket_class"] = socket_factory end if proxy @@ -131,13 +136,15 @@ def initialize(uri, end if @proxy - http_client_options["proxy"] = { + base_http_client_options["proxy"] = { :proxy_address => @proxy.host, :proxy_port => @proxy.port, } end - @http_client = HTTP::Client.new(http_client_options) + options = http_client_options.is_a?(Hash) ? base_http_client_options.merge(http_client_options) : base_http_client_options + + @http_client = HTTP::Client.new(options) .follow .timeout({ read: read_timeout, diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 12fde0d..62c4968 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -738,6 +738,155 @@ def test_object.to_s end end + describe "http_client_options precedence" do + it "allows socket_factory to be set via individual parameter" do + mock_socket_factory = double("MockSocketFactory") + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + # We can't easily test socket creation without actually making a connection, + # but we can verify the options contain the socket_class + client = nil + expect { + client = subject.new(server.base_uri, socket_factory: mock_socket_factory) + }.not_to raise_error + + # Access the internal HTTP client to verify socket_class was set + expect(client.instance_variable_get(:@http_client).default_options.socket_class).to eq(mock_socket_factory) + + client.close + end + end + + it "allows proxy to be set via individual parameter" do + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, simple_event_1_text, keep_open: false) + end + + with_server(StubProxyServer.new) do |proxy| + event_sink = Queue.new + client = subject.new(server.base_uri, proxy: proxy.base_uri) do |c| + c.on_event { |event| event_sink << event } + end + + with_client(client) do |c| + expect(event_sink.pop).to eq(simple_event_1) + expect(proxy.request_count).to eq(1) + end + end + end + end + + it "allows http_client_options to override socket_factory" do + individual_socket_factory = double("IndividualSocketFactory") + override_socket_factory = double("OverrideSocketFactory") + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + # http_client_options should take precedence over individual parameter + client = nil + expect { + client = subject.new(server.base_uri, + socket_factory: individual_socket_factory, + http_client_options: {"socket_class" => override_socket_factory}) + }.not_to raise_error + + # Verify that the override socket factory was used, not the individual one + expect(client.instance_variable_get(:@http_client).default_options.socket_class).to eq(override_socket_factory) + + client.close + end + end + + it "allows http_client_options to override proxy settings" do + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, simple_event_1_text, keep_open: false) + end + + with_server(StubProxyServer.new) do |individual_proxy| + with_server(StubProxyServer.new) do |override_proxy| + event_sink = Queue.new + client = subject.new(server.base_uri, + proxy: individual_proxy.base_uri, + http_client_options: {"proxy" => { + :proxy_address => override_proxy.base_uri.host, + :proxy_port => override_proxy.base_uri.port, + }}) do |c| + c.on_event { |event| event_sink << event } + end + + with_client(client) do |c| + expect(event_sink.pop).to eq(simple_event_1) + # The override proxy should be used, not the individual one + expect(override_proxy.request_count).to eq(1) + expect(individual_proxy.request_count).to eq(0) + end + end + end + end + end + + it "merges http_client_options with base options when both socket_factory and other options are provided" do + socket_factory = double("SocketFactory") + ssl_options = { verify_mode: 0 } # OpenSSL::SSL::VERIFY_NONE equivalent + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + # Should include both socket_factory from individual param and ssl from http_client_options + client = nil + expect { + client = subject.new(server.base_uri, + socket_factory: socket_factory, + http_client_options: {"ssl" => ssl_options}) + }.not_to raise_error + + # Verify both options are present + http_options = client.instance_variable_get(:@http_client).default_options + expect(http_options.socket_class).to eq(socket_factory) + expect(http_options.ssl).to eq(ssl_options) + + client.close + end + end + end + + describe "http_client_options SSL pass-through" do + it "passes SSL verification options through http_client_options" do + ssl_options = { + verify_mode: 0, # OpenSSL::SSL::VERIFY_NONE equivalent + verify_hostname: false, + } + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + client = nil + expect { + client = subject.new(server.base_uri, + http_client_options: {"ssl" => ssl_options}) + }.not_to raise_error + + # Verify SSL options are passed through + expect(client.instance_variable_get(:@http_client).default_options.ssl).to eq(ssl_options) + + client.close + end + end + end + describe "retry parameter" do it "defaults to true (retries enabled)" do events_body = simple_event_1_text