diff --git a/Gemfile b/Gemfile index db4401116..889ffcfcb 100644 --- a/Gemfile +++ b/Gemfile @@ -11,8 +11,8 @@ group :test do gem 'rubocop' gem 'standard' gem 'simplecov', '>= 0.21.2', require: false - gem 'vcr' - gem 'webmock' + gem 'vcr', '>= 6.4' + gem 'webmock', '>= 3.14' gem 'cucumber' end diff --git a/Gemfile.lock b/Gemfile.lock index 12f588e0b..ce3af9cc1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,11 +3,14 @@ PATH specs: pubnub (6.0.2) addressable (>= 2.0.0) + base64 concurrent-ruby (~> 1.3.4) concurrent-ruby-edge (~> 0.7.1) dry-validation (~> 1.0) - httpclient (~> 2.8, >= 2.8.3) + httpx (>= 1.0) json (>= 2.2.0, < 3) + logger + ostruct timers (>= 4.3.0) GEM @@ -17,7 +20,7 @@ GEM public_suffix (>= 2.0.2, < 7.0) ast (2.4.2) awesome_print (1.9.2) - base64 (0.2.0) + base64 (0.3.0) bigdecimal (3.1.8) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) @@ -94,14 +97,18 @@ GEM zeitwerk (~> 2.6) ffi (1.17.0) hashdiff (1.1.1) - httpclient (2.8.3) + http-2 (1.1.3) + httpx (1.7.8) + http-2 (>= 1.1.3) interception (0.5) json (2.7.2) language_server-protocol (3.17.0.3) lint_roller (1.1.0) + logger (1.7.0) method_source (1.1.0) mini_mime (1.1.5) multi_test (1.1.0) + ostruct (0.6.3) parallel (1.26.3) parser (3.3.5.0) ast (~> 2.4.1) @@ -174,8 +181,7 @@ GEM ffi (~> 1.1) timers (4.3.5) unicode-display_width (2.6.0) - vcr (6.3.1) - base64 + vcr (6.4.0) webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -200,8 +206,8 @@ DEPENDENCIES rubocop simplecov (>= 0.21.2) standard - vcr - webmock + vcr (>= 6.4) + webmock (>= 3.14) BUNDLED WITH 2.4.20 diff --git a/lib/pubnub.rb b/lib/pubnub.rb index 121b727a6..124c41a59 100644 --- a/lib/pubnub.rb +++ b/lib/pubnub.rb @@ -5,7 +5,9 @@ require 'openssl' require 'timers' -require 'httpclient' +require 'httpx' +require 'pubnub/http_response' +require 'pubnub/http_dispatcher' require 'logger' require 'dry-validation' require 'cgi' diff --git a/lib/pubnub/client.rb b/lib/pubnub/client.rb index ed70a3edc..0a7de745b 100644 --- a/lib/pubnub/client.rb +++ b/lib/pubnub/client.rb @@ -268,11 +268,11 @@ def request_dispatcher(origin, event_type, sync) if sync @env[:req_dispatchers_pool][:sync][origin] ||= {} @env[:req_dispatchers_pool][:sync][origin][event_type] ||= - setup_httpclient(event_type) + setup_httpx_session(event_type) else @env[:req_dispatchers_pool][:async][origin] ||= {} @env[:req_dispatchers_pool][:async][origin][event_type] ||= - setup_httpclient(event_type) + setup_httpx_session(event_type) end end @@ -359,33 +359,43 @@ def create_state_pools(event) @env[:state][event.origin] ||= {} end - def setup_httpclient(event_type) - hc = if ENV["HTTP_PROXY"] - HTTPClient.new(ENV["HTTP_PROXY"]) - else - HTTPClient.new - end - - case event_type - when :subscribe_event - hc.connect_timeout = @env[:s_open_timeout] - hc.send_timeout = @env[:s_send_timeout] - hc.receive_timeout = @env[:s_read_timeout] - unless @env[:disable_keepalive] || @env[:disable_subscribe_keepalive] - hc.keep_alive_timeout = @env[:idle_timeout] - hc.tcp_keepalive = true + def setup_httpx_session(event_type) + timeout_opts = case event_type + when :subscribe_event + { + connect_timeout: @env[:s_open_timeout], + write_timeout: @env[:s_send_timeout] || @env[:s_open_timeout], + read_timeout: @env[:s_read_timeout] + } + when :single_event + { + connect_timeout: @env[:open_timeout], + write_timeout: @env[:send_timeout] || @env[:open_timeout], + read_timeout: @env[:read_timeout] + } end - when :single_event - hc.connect_timeout = @env[:open_timeout] - hc.send_timeout = @env[:send_timeout] - hc.receive_timeout = @env[:read_timeout] - unless @env[:disable_keepalive] || @env[:disable_non_subscribe_keepalive] - hc.keep_alive_timeout = @env[:idle_timeout] - hc.tcp_keepalive = true + + keepalive_enabled = case event_type + when :subscribe_event + !(@env[:disable_keepalive] || @env[:disable_subscribe_keepalive]) + when :single_event + !(@env[:disable_keepalive] || @env[:disable_non_subscribe_keepalive]) end + + if keepalive_enabled + timeout_opts[:keep_alive_timeout] = @env[:idle_timeout] + end + + options = { timeout: timeout_opts } + httpx = HTTPX.plugin(:persistent) + + if ENV["HTTP_PROXY"] + options[:proxy] = { uri: ENV["HTTP_PROXY"] } + httpx = httpx.plugin(:proxy) end - hc + session = httpx.with(**options) + HttpDispatcher.new(session, keepalive_enabled: keepalive_enabled) end def validate!(env) diff --git a/lib/pubnub/event.rb b/lib/pubnub/event.rb index 6eb72f9d1..6c74cbc8f 100644 --- a/lib/pubnub/event.rb +++ b/lib/pubnub/event.rb @@ -251,7 +251,7 @@ def error_envelope(parsed_response, error, req_res_objects) when JSON::ParserError error_category = Pubnub::Constants::STATUS_NON_JSON_RESPONSE code = req_res_objects[:response].code - when HTTPClient::TimeoutError + when HTTPX::TimeoutError error_category = Pubnub::Constants::STATUS_TIMEOUT code = 408 when OpenSSL::SSL::SSLError diff --git a/lib/pubnub/event/formatter.rb b/lib/pubnub/event/formatter.rb index 67830d403..900d5e646 100644 --- a/lib/pubnub/event/formatter.rb +++ b/lib/pubnub/event/formatter.rb @@ -5,7 +5,7 @@ class Event # Module that holds formatters for events module EFormatter def format_envelopes(response, request) - if response.is_a?(HTTPClient::TimeoutError) || response.is_a?(OpenSSL::SSL::SSLError) + if response.is_a?(HTTPX::TimeoutError) || response.is_a?(OpenSSL::SSL::SSLError) return error_envelope(nil, response, request: request, diff --git a/lib/pubnub/http_dispatcher.rb b/lib/pubnub/http_dispatcher.rb new file mode 100644 index 000000000..f81d2fce7 --- /dev/null +++ b/lib/pubnub/http_dispatcher.rb @@ -0,0 +1,40 @@ +module Pubnub + class HttpDispatcher + attr_reader :tcp_keepalive + + def initialize(httpx_session, keepalive_enabled: true) + @session = httpx_session + @tcp_keepalive = keepalive_enabled + end + + def get(url, header: {}) + execute { @session.get(url, headers: header) } + end + + def post(url, body: '', header: {}) + execute { @session.post(url, headers: header, body: body) } + end + + def patch(url, body: '', header: {}) + execute { @session.patch(url, headers: header, body: body) } + end + + def delete(url, header: {}) + execute { @session.delete(url, headers: header) } + end + + def reset_all + @session.close + end + + private + + def execute + response = yield + if response.is_a?(HTTPX::ErrorResponse) + raise response.error + end + Pubnub::HttpResponse.new(response) + end + end +end diff --git a/lib/pubnub/http_response.rb b/lib/pubnub/http_response.rb new file mode 100644 index 000000000..4173b6c58 --- /dev/null +++ b/lib/pubnub/http_response.rb @@ -0,0 +1,12 @@ +module Pubnub + class HttpResponse + attr_reader :body, :code, :http_version + alias status_code code + + def initialize(httpx_response) + @code = httpx_response.status + @body = httpx_response.body.to_s + @http_version = httpx_response.version + end + end +end diff --git a/lib/pubnub/pam.rb b/lib/pubnub/pam.rb index 7115f2da3..8dcb51b2b 100644 --- a/lib/pubnub/pam.rb +++ b/lib/pubnub/pam.rb @@ -77,7 +77,7 @@ def error_envelope(parsed_response, error, req_res_objects) when JSON::ParserError error_category = Pubnub::Constants::STATUS_NON_JSON_RESPONSE code = req_res_objects[:response].code - when HTTPClient::TimeoutError + when HTTPX::TimeoutError error_category = Pubnub::Constants::STATUS_TIMEOUT code = 408 else diff --git a/lib/pubnub/subscribe_event.rb b/lib/pubnub/subscribe_event.rb index bc7bb8a7f..1a637825c 100644 --- a/lib/pubnub/subscribe_event.rb +++ b/lib/pubnub/subscribe_event.rb @@ -69,6 +69,7 @@ def send_request(retries = 0) begin req = request_dispatcher.get(uri.to_s) + if retries > 0 @app.subscriber.announce_status(announcement_type: Pubnub::Constants::RECONNECTED_ANNOUNCEMENT, event: @event, diff --git a/pubnub.gemspec b/pubnub.gemspec index 2951fa2f0..71f17f6df 100644 --- a/pubnub.gemspec +++ b/pubnub.gemspec @@ -19,10 +19,13 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 3.0.0' spec.add_dependency 'addressable', '>= 2.0.0' + spec.add_dependency 'base64' + spec.add_dependency 'logger' + spec.add_dependency 'ostruct' spec.add_dependency 'concurrent-ruby', '~> 1.3.4' spec.add_dependency 'concurrent-ruby-edge', '~> 0.7.1' spec.add_dependency 'dry-validation', '~> 1.0' - spec.add_dependency 'httpclient', '~> 2.8', '>= 2.8.3' + spec.add_dependency 'httpx', '>= 1.0' spec.add_dependency 'json', '>= 2.2.0', '< 3' spec.add_dependency 'timers', '>= 4.3.0' end diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index 3680942a1..be4b6ff8a 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -2,6 +2,7 @@ require "rr" require "stringio" require "webmock/rspec" +require "httpx/adapters/webmock" require "vcr" require "pry" require "spec_expectations" @@ -67,6 +68,7 @@ def loop_it(interval, time_limit) config.include AsyncHelper config.mock_framework = :rspec config.tty = true + config.filter_run_excluding integration: true logfile = File.open(File.expand_path("../../test.log", __FILE__), "a") logfile.sync = true diff --git a/spec/lib/client_spec.rb b/spec/lib/client_spec.rb index 0adededeb..cdc62d5ff 100644 --- a/spec/lib/client_spec.rb +++ b/spec/lib/client_spec.rb @@ -278,5 +278,6 @@ eventually { expect(pubnub.env[:req_dispatchers_pool][:async]["ps.pndsn.com"][:single_event].tcp_keepalive).to eq false } end end + end end diff --git a/spec/lib/events/grant_spec.rb b/spec/lib/events/grant_spec.rb index b77bad380..a6509c7f0 100644 --- a/spec/lib/events/grant_spec.rb +++ b/spec/lib/events/grant_spec.rb @@ -39,19 +39,13 @@ end end - [ - HTTPClient::ConnectTimeoutError, - HTTPClient::ReceiveTimeoutError, - HTTPClient::SendTimeoutError, - ].each do |error_class| - it "forms valid ErrorEnvelope on #{error_class}" do - allow_any_instance_of(HTTPClient).to receive(:get).and_return error_class.new + it "forms valid ErrorEnvelope on HTTPX::TimeoutError" do + allow_any_instance_of(Pubnub::HttpDispatcher).to receive(:get).and_raise(HTTPX::TimeoutError.new(nil, "timeout")) - expect(envelope.is_a?(Pubnub::ErrorEnvelope)).to eq true - expect(envelope.status[:code]).to eq 408 - expect(envelope.status[:category]).to eq Pubnub::Constants::STATUS_TIMEOUT - expect(envelope.status).to satisfies_schema Pubnub::Schemas::Envelope::StatusSchema.new - end + expect(envelope.is_a?(Pubnub::ErrorEnvelope)).to eq true + expect(envelope.status[:code]).to eq 408 + expect(envelope.status[:category]).to eq Pubnub::Constants::STATUS_TIMEOUT + expect(envelope.status).to satisfies_schema Pubnub::Schemas::Envelope::StatusSchema.new end end end diff --git a/spec/lib/events/timeout_handling_spec.rb b/spec/lib/events/timeout_handling_spec.rb index 910109c83..0bd3c1ce8 100644 --- a/spec/lib/events/timeout_handling_spec.rb +++ b/spec/lib/events/timeout_handling_spec.rb @@ -12,19 +12,13 @@ end let(:envelope) { pubnub.time.value } - [ - HTTPClient::ConnectTimeoutError, - HTTPClient::ReceiveTimeoutError, - HTTPClient::SendTimeoutError, - ].each do |error_class| - it "forms valid ErrorEnvelope on #{error_class}" do - allow_any_instance_of(HTTPClient).to receive(:get).and_return error_class.new + it "forms valid ErrorEnvelope on HTTPX::TimeoutError" do + allow_any_instance_of(Pubnub::HttpDispatcher).to receive(:get).and_raise(HTTPX::TimeoutError.new(nil, "timeout")) - expect(envelope.is_a?(Pubnub::ErrorEnvelope)).to eq true - expect(envelope.status[:code]).to eq 408 - expect(envelope.status[:category]).to eq Pubnub::Constants::STATUS_TIMEOUT - expect(envelope.status).to satisfies_schema Pubnub::Schemas::Envelope::StatusSchema.new - end + expect(envelope.is_a?(Pubnub::ErrorEnvelope)).to eq true + expect(envelope.status[:code]).to eq 408 + expect(envelope.status[:category]).to eq Pubnub::Constants::STATUS_TIMEOUT + expect(envelope.status).to satisfies_schema Pubnub::Schemas::Envelope::StatusSchema.new end end end