diff --git a/lib/httpi.rb b/lib/httpi.rb index 514ef14..6e11f24 100644 --- a/lib/httpi.rb +++ b/lib/httpi.rb @@ -86,11 +86,11 @@ module HTTPI DEFAULT_LOG_LEVEL = :debug class Error < StandardError; end - class TimeoutError < Error; end class NotSupportedError < Error; end class NotImplementedError < Error; end module ConnectionError; end + module TimeoutError; end class SSLError < Error def initialize(message = nil, original = $!) diff --git a/lib/httpi/adapter/curb.rb b/lib/httpi/adapter/curb.rb index f6afb41..2eb5b7f 100644 --- a/lib/httpi/adapter/curb.rb +++ b/lib/httpi/adapter/curb.rb @@ -46,6 +46,9 @@ def request(method) rescue Curl::Err::ConnectionFailedError # connection refused $!.extend ConnectionError raise + rescue Curl::Err::TimeoutError + $!.extend TimeoutError + raise end private @@ -72,8 +75,9 @@ def setup_client def basic_setup @client.url = @request.url.to_s @client.proxy_url = @request.proxy.to_s if @request.proxy - @client.timeout = @request.read_timeout if @request.read_timeout - @client.connect_timeout = @request.open_timeout if @request.open_timeout + read_or_write_timeout = @request.read_timeout || @request.write_timeout + @client.timeout_ms = read_or_write_timeout * 1000 if read_or_write_timeout + @client.connect_timeout_ms = @request.open_timeout * 1000 if @request.open_timeout @client.headers = @request.headers.to_hash @client.verbose = false # cURL workaround diff --git a/lib/httpi/adapter/em_http.rb b/lib/httpi/adapter/em_http.rb index 3aeb58d..e2fc0c6 100644 --- a/lib/httpi/adapter/em_http.rb +++ b/lib/httpi/adapter/em_http.rb @@ -29,6 +29,8 @@ module Adapter # are supported by em-httprequest but not HTTPI. class EmHttpRequest < Base + class EmHttpTimeoutError < StandardError; end # Generic error for timeouts + register :em_http, :deps => %w(em-synchrony em-synchrony/em-http em-http) def initialize(request) @@ -48,6 +50,9 @@ def cert_directory # @see HTTPI.request def request(method) _request { |options| @client.send method, options } + rescue EmHttpTimeoutError + $!.extend TimeoutError + raise end private @@ -69,10 +74,11 @@ def _request end def connection_options - options = { - :connect_timeout => @request.open_timeout, - :inactivity_timeout => @request.read_timeout - } + options = {} + + read_or_write_timeout = @request.read_timeout || @request.write_timeout + options[:inactivity_timeout] = read_or_write_timeout if read_or_write_timeout + options[:connect_timeout] = @request.open_timeout if @request.open_timeout options[:proxy] = proxy_options if @request.proxy @@ -105,7 +111,7 @@ def setup_http_auth(options) end def respond_with(http, start_time) - raise TimeoutError, "EM-HTTP-Request connection timed out: #{Time.now - start_time} sec" if http.response_header.status.zero? + raise EmHttpTimeoutError, "EM-HTTP-Request connection timed out: #{Time.now - start_time} sec" if http.response_header.status.zero? Response.new http.response_header.status, convert_headers(http.response_header), http.response diff --git a/lib/httpi/adapter/excon.rb b/lib/httpi/adapter/excon.rb index b80396b..19f0eb6 100644 --- a/lib/httpi/adapter/excon.rb +++ b/lib/httpi/adapter/excon.rb @@ -31,6 +31,9 @@ def request(method) $!.extend ConnectionError end raise + rescue ::Excon::Error::Timeout + $!.extend TimeoutError + raise end private @@ -58,6 +61,7 @@ def client_opts opts[:user], opts[:password] = *@request.auth.credentials if @request.auth.basic? opts[:connect_timeout] = @request.open_timeout if @request.open_timeout opts[:read_timeout] = @request.read_timeout if @request.read_timeout + opts[:write_timeout] = @request.write_timeout if @request.write_timeout opts[:response_block] = @request.on_body if @request.on_body opts[:proxy] = @request.proxy if @request.proxy diff --git a/lib/httpi/adapter/http.rb b/lib/httpi/adapter/http.rb index e60e787..15ea8c9 100644 --- a/lib/httpi/adapter/http.rb +++ b/lib/httpi/adapter/http.rb @@ -32,13 +32,13 @@ def request(method) unless ::HTTP::Request::METHODS.include? method raise NotSupportedError, "http.rb does not support custom HTTP methods" end - response = begin - @client.send(method, @request.url, :body => @request.body) - rescue OpenSSL::SSL::SSLError - raise SSLError - end - + response = @client.send(method, @request.url, :body => @request.body) Response.new(response.code, response.headers.to_h, response.body.to_s) + rescue OpenSSL::SSL::SSLError + raise SSLError + rescue ::HTTP::TimeoutError + $!.extend TimeoutError + raise end private @@ -73,6 +73,12 @@ def create_client client = client.via(@request.proxy.host, @request.proxy.port, @request.proxy.user, @request.proxy.password) end + timeouts = {} + timeouts[:connect] = @request.open_timeout if @request.open_timeout + timeouts[:read] = @request.read_timeout if @request.read_timeout + timeouts[:write] = @request.write_timeout if @request.write_timeout + client = client.timeout(timeouts) if timeouts.any? + client.headers(@request.headers) end end diff --git a/lib/httpi/adapter/httpclient.rb b/lib/httpi/adapter/httpclient.rb index 97cd56b..7175aa9 100644 --- a/lib/httpi/adapter/httpclient.rb +++ b/lib/httpi/adapter/httpclient.rb @@ -29,6 +29,9 @@ def request(method) rescue Errno::ECONNREFUSED # connection refused $!.extend ConnectionError raise + rescue ::HTTPClient::TimeoutError + $!.extend TimeoutError + raise end private @@ -45,6 +48,7 @@ def basic_setup @client.proxy = @request.proxy if @request.proxy @client.connect_timeout = @request.open_timeout if @request.open_timeout @client.receive_timeout = @request.read_timeout if @request.read_timeout + @client.send_timeout = @request.write_timeout if @request.write_timeout end def setup_auth diff --git a/lib/httpi/adapter/net_http.rb b/lib/httpi/adapter/net_http.rb index 9aa3855..3f57e3b 100644 --- a/lib/httpi/adapter/net_http.rb +++ b/lib/httpi/adapter/net_http.rb @@ -52,6 +52,9 @@ def request(method) rescue Errno::ECONNREFUSED # connection refused $!.extend ConnectionError raise + rescue ::Timeout::Error + $!.extend TimeoutError + raise end private @@ -155,6 +158,13 @@ def setup_client @client.use_ssl = @request.ssl? @client.open_timeout = @request.open_timeout if @request.open_timeout @client.read_timeout = @request.read_timeout if @request.read_timeout + if @request.write_timeout + if @client.respond_to?(:write_timeout=) # Expected to appear in Ruby 2.6 + @client.write_timeout = @request.write_timeout + else + raise NotSupportedError, "Net::HTTP supports write_timeout starting from Ruby 2.6" + end + end end def setup_ssl_auth diff --git a/lib/httpi/adapter/net_http_persistent.rb b/lib/httpi/adapter/net_http_persistent.rb index 554cc4e..ba440c9 100644 --- a/lib/httpi/adapter/net_http_persistent.rb +++ b/lib/httpi/adapter/net_http_persistent.rb @@ -9,6 +9,17 @@ class NetHTTPPersistent < NetHTTP register :net_http_persistent, :deps => %w(net/http/persistent) + # Executes arbitrary HTTP requests. + # @see HTTPI.request + def request(method) + super + rescue Net::HTTP::Persistent::Error => e + if !e.message.nil? && e.message =~ /Timeout/ + $!.extend TimeoutError + end + raise + end + private def create_client @@ -32,6 +43,7 @@ def setup_client @client.open_timeout = @request.open_timeout if @request.open_timeout @client.read_timeout = @request.read_timeout if @request.read_timeout + raise NotSupportedError, "Net::HTTP::Persistent does not support write_timeout" if @request.write_timeout end def thread_key diff --git a/lib/httpi/request.rb b/lib/httpi/request.rb index 8c0d071..8eebf9a 100644 --- a/lib/httpi/request.rb +++ b/lib/httpi/request.rb @@ -11,7 +11,7 @@ module HTTPI class Request # Available attribute writers. - ATTRIBUTES = [:url, :proxy, :headers, :body, :open_timeout, :read_timeout, :follow_redirect, :redirect_limit, :query] + ATTRIBUTES = [:url, :proxy, :headers, :body, :open_timeout, :read_timeout, :write_timeout, :follow_redirect, :redirect_limit, :query] # Accepts a Hash of +args+ to mass assign attributes and authentication credentials. def initialize(args = {}) @@ -90,7 +90,7 @@ def set_cookies(object_or_array) headers["Cookie"] = cookies if cookies end - attr_accessor :open_timeout, :read_timeout + attr_accessor :open_timeout, :read_timeout, :write_timeout attr_reader :body # Sets a body request given a String or a Hash. diff --git a/spec/httpi/adapter/curb_spec.rb b/spec/httpi/adapter/curb_spec.rb index b624447..ec931b7 100644 --- a/spec/httpi/adapter/curb_spec.rb +++ b/spec/httpi/adapter/curb_spec.rb @@ -146,29 +146,36 @@ end end - describe "timeout" do + describe "timeout_ms" do it "is not set unless it's specified" do - curb.expects(:timeout=).never + curb.expects(:timeout_ms=).never adapter.request(:get) end - it "is set if specified" do + it "is set if specified read_timeout" do request.read_timeout = 30 - curb.expects(:timeout=).with(request.read_timeout) + curb.expects(:timeout_ms=).with(30_000) + + adapter.request(:get) + end + + it "is set if specified write_timeout" do + request.write_timeout = 30 + curb.expects(:timeout_ms=).with(30_000) adapter.request(:get) end end - describe "connect_timeout" do + describe "connect_timeout_ms" do it "is not set unless it's specified" do - curb.expects(:connect_timeout=).never + curb.expects(:connect_timeout_ms=).never adapter.request(:get) end it "is set if specified" do request.open_timeout = 30 - curb.expects(:connect_timeout=).with(30) + curb.expects(:connect_timeout_ms=).with(30_000) adapter.request(:get) end diff --git a/spec/httpi/adapter/em_http_spec.rb b/spec/httpi/adapter/em_http_spec.rb index a171138..e0a6eda 100644 --- a/spec/httpi/adapter/em_http_spec.rb +++ b/spec/httpi/adapter/em_http_spec.rb @@ -89,15 +89,12 @@ end it "sets host, port and authorization" do - url = 'http://example.com:80' - + url = "http://example.com:80" connection_options = { - :connect_timeout => nil, - :inactivity_timeout => nil, - :proxy => { - :host => 'proxy-host.com', - :port => 443, - :authorization => ['username', 'password'] + :proxy => { + :host => "proxy-host.com", + :port => 443, + :authorization => ["username", "password"] } } @@ -111,8 +108,8 @@ it "is passed as a connection option" do request.open_timeout = 30 - url = 'http://example.com:80' - connection_options = { :connect_timeout => 30, :inactivity_timeout => nil } + url = "http://example.com:80" + connection_options = { connect_timeout: 30 } EventMachine::HttpRequest.expects(:new).with(url, connection_options) @@ -121,11 +118,22 @@ end describe "receive_timeout" do - it "is passed as a connection option" do + it "is passed as a connection option (when read_timeout specified)" do request.read_timeout = 60 - url = 'http://example.com:80' - connection_options = { :connect_timeout => nil, :inactivity_timeout => 60 } + url = "http://example.com:80" + connection_options = { inactivity_timeout: 60 } + + EventMachine::HttpRequest.expects(:new).with(url, connection_options) + + adapter + end + + it "is passed as a connection option (when write_timeout specified)" do + request.write_timeout = 60 + + url = "http://example.com:80" + connection_options = { inactivity_timeout: 60 } EventMachine::HttpRequest.expects(:new).with(url, connection_options) diff --git a/spec/httpi/adapter/excon_spec.rb b/spec/httpi/adapter/excon_spec.rb index 52af8c2..e26ab32 100644 --- a/spec/httpi/adapter/excon_spec.rb +++ b/spec/httpi/adapter/excon_spec.rb @@ -1,124 +1,28 @@ require "spec_helper" -require "integration/support/server" - -describe HTTPI::Adapter::Excon do - - subject(:adapter) { :excon } - - context "http requests" do - before :all do - @server = IntegrationServer.run - end - - after :all do - @server.stop - end - - it "sends and receives HTTP headers" do - request = HTTPI::Request.new(@server.url + "x-header") - request.headers["X-Header"] = "HTTPI" - - response = HTTPI.get(request, adapter) - expect(response.body).to include("HTTPI") - end - - it "executes GET requests" do - response = HTTPI.get(@server.url, adapter) - expect(response.body).to eq("get") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes POST requests" do - response = HTTPI.post(@server.url, "xml", adapter) - expect(response.body).to eq("post") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes HEAD requests" do - response = HTTPI.head(@server.url, adapter) - expect(response.code).to eq(200) - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes PUT requests" do - response = HTTPI.put(@server.url, "xml", adapter) - expect(response.body).to eq("put") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes DELETE requests" do - response = HTTPI.delete(@server.url, adapter) - expect(response.body).to eq("delete") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "supports basic authentication" do - request = HTTPI::Request.new(@server.url + "basic-auth") - request.auth.basic("admin", "secret") - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("basic-auth") - end - - it "does not support ntlm authentication" do - request = HTTPI::Request.new(@server.url + "ntlm-auth") - request.auth.ntlm("tester", "vReqSoafRe5O") - - expect { HTTPI.get(request, adapter) }. - to raise_error(HTTPI::NotSupportedError, /does not support NTLM authentication/) - end - - it "supports disabling verify mode" do - request = HTTPI::Request.new(@server.url) - request.auth.ssl.verify_mode = :none - adapter_class = HTTPI::Adapter.load(adapter).new(request) - expect(adapter_class.client.data[:ssl_verify_peer]).to eq(false) - end - end - - # it does not support digest auth - - if RUBY_PLATFORM =~ /java/ - pending "Puma Server complains: SSL not supported on JRuby" - else - context "https requests" do - before :all do - @server = IntegrationServer.run(:ssl => true) - end - after :all do - @server.stop - end - - # it does not raise when no certificate was set up - it "works when no client cert is specified" do - request = HTTPI::Request.new(@server.url) - request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("get") - end - - it "works with client cert and key provided as file path" do - request = HTTPI::Request.new(@server.url) - request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file - request.auth.ssl.cert_file = "spec/fixtures/client_cert.pem" - request.auth.ssl.cert_key_file = "spec/fixtures/client_key.pem" - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("get") - end - - it "works with client cert and key set directly" do - request = HTTPI::Request.new(@server.url) - - request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file - request.auth.ssl.cert = OpenSSL::X509::Certificate.new File.open("spec/fixtures/client_cert.pem").read - request.auth.ssl.cert_key = OpenSSL::PKey.read File.open("spec/fixtures/client_key.pem").read - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("get") +require "httpi/adapter/excon" +require "httpi/request" + +begin + HTTPI::Adapter.load_adapter(:excon) + + describe HTTPI::Adapter::Excon do + let(:adapter) { HTTPI::Adapter::Excon.new(request) } + let(:request) { HTTPI::Request.new("http://example.com") } + + describe "settings" do + describe "connect_timeout, read_timeout, write_timeout" do + it "are passed as connection options" do + request.open_timeout = 30 + request.read_timeout = 40 + request.write_timeout = 50 + + expect(adapter.client.data).to include( + connect_timeout: 30, + read_timeout: 40, + write_timeout: 50 + ) + end end end end - end diff --git a/spec/httpi/adapter/http_spec.rb b/spec/httpi/adapter/http_spec.rb index 34b482d..aa3e842 100644 --- a/spec/httpi/adapter/http_spec.rb +++ b/spec/httpi/adapter/http_spec.rb @@ -1,101 +1,28 @@ require "spec_helper" -require "integration/support/server" - -describe HTTPI::Adapter::HTTP do - - subject(:adapter) { :http } - - context "http requests" do - before :all do - @server = IntegrationServer.run - end - - after :all do - @server.stop - end - - it "sends and receives HTTP headers" do - request = HTTPI::Request.new(@server.url + "x-header") - request.headers["X-Header"] = "HTTPI" - - response = HTTPI.get(request, adapter) - expect(response.body).to include("HTTPI") - end - - it "executes GET requests" do - response = HTTPI.get(@server.url, adapter) - expect(response.body).to eq("get") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes POST requests" do - response = HTTPI.post(@server.url, "xml", adapter) - expect(response.body).to eq("post") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes HEAD requests" do - response = HTTPI.head(@server.url, adapter) - expect(response.code).to eq(200) - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes PUT requests" do - response = HTTPI.put(@server.url, "xml", adapter) - expect(response.body).to eq("put") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes DELETE requests" do - response = HTTPI.delete(@server.url, adapter) - expect(response.body).to eq("delete") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "supports basic authentication" do - request = HTTPI::Request.new(@server.url + "basic-auth") - request.auth.basic("admin", "secret") - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("basic-auth") - end - - it "does not support digest authentication" do - request = HTTPI::Request.new(@server.url + "digest-auth") - request.auth.digest("admin", "secret") - - expect { HTTPI.get(request, adapter) }. - to raise_error(HTTPI::NotSupportedError, /does not support HTTP digest authentication/) - end - - it "does not support ntlm authentication" do - request = HTTPI::Request.new(@server.url + "ntlm-auth") - request.auth.ntlm("tester", "vReqSoafRe5O") - - expect { HTTPI.get(request, adapter) }. - to raise_error(HTTPI::NotSupportedError, /does not support NTLM digest authentication/) - end - end - - if RUBY_PLATFORM =~ /java/ - pending "Puma Server complains: SSL not supported on JRuby" - else - context "https requests" do - before :all do - @server = IntegrationServer.run(:ssl => true) - end - after :all do - @server.stop - end - - it "works when set up properly" do - request = HTTPI::Request.new(@server.url) - request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("get") +require "httpi/adapter/http" +require "httpi/request" + +begin + HTTPI::Adapter.load_adapter(:http) + + describe HTTPI::Adapter::HTTP do + let(:adapter) { HTTPI::Adapter::HTTP.new(request) } + let(:request) { HTTPI::Request.new("http://example.com") } + + describe "settings" do + describe "connect_timeout, read_timeout, write_timeout" do + it "are being set on the client" do + request.open_timeout = 30 + request.read_timeout = 40 + request.write_timeout = 50 + + expect(adapter.client.default_options.timeout_options).to eq( + connect_timeout: 30, + read_timeout: 40, + write_timeout: 50 + ) + end end end end - end diff --git a/spec/httpi/adapter/httpclient_spec.rb b/spec/httpi/adapter/httpclient_spec.rb index 41ca299..e6371a3 100644 --- a/spec/httpi/adapter/httpclient_spec.rb +++ b/spec/httpi/adapter/httpclient_spec.rb @@ -100,6 +100,20 @@ end end + describe "send_timeout" do + it "is not set unless specified" do + httpclient.expects(:send_timeout=).never + adapter.request(:get) + end + + it "is set if specified" do + request.write_timeout = 30 + + httpclient.expects(:send_timeout=).with(30) + adapter.request(:get) + end + end + describe "set_auth" do it "is set for HTTP basic auth" do request.auth.basic "username", "password" diff --git a/spec/httpi/adapter/net_http_persistent_spec.rb b/spec/httpi/adapter/net_http_persistent_spec.rb index 8f02ec8..9ed7a5f 100644 --- a/spec/httpi/adapter/net_http_persistent_spec.rb +++ b/spec/httpi/adapter/net_http_persistent_spec.rb @@ -1,96 +1,46 @@ require "spec_helper" -require "integration/support/server" +require "httpi/adapter/net_http_persistent" +require "httpi/request" -describe HTTPI::Adapter::NetHTTPPersistent do +begin + HTTPI::Adapter.load_adapter(:net_http_persistent) - subject(:adapter) { :net_http_persistent } + describe HTTPI::Adapter::NetHTTPPersistent do + let(:adapter) { HTTPI::Adapter::NetHTTPPersistent.new(request) } + let(:request) { HTTPI::Request.new("http://example.com") } - context "http requests" do - before :all do - @server = IntegrationServer.run - end - - after :all do - @server.stop - end - - it "sends and receives HTTP headers" do - request = HTTPI::Request.new(@server.url + "x-header") - request.headers["X-Header"] = "HTTPI" - - response = HTTPI.get(request, adapter) - expect(response.body).to include("HTTPI") - end - - it "executes GET requests" do - response = HTTPI.get(@server.url, adapter) - expect(response.body).to eq("get") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes POST requests" do - response = HTTPI.post(@server.url, "xml", adapter) - expect(response.body).to eq("post") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes HEAD requests" do - response = HTTPI.head(@server.url, adapter) - expect(response.code).to eq(200) - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes PUT requests" do - response = HTTPI.put(@server.url, "xml", adapter) - expect(response.body).to eq("put") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes DELETE requests" do - response = HTTPI.delete(@server.url, adapter) - expect(response.body).to eq("delete") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "supports basic authentication" do - request = HTTPI::Request.new(@server.url + "basic-auth") - request.auth.basic("admin", "secret") + let(:response) { + Object.new.tap do |r| + r.stubs(:code).returns(200) + r.stubs(:body).returns("abc") + r.stubs(:to_hash).returns({"Content-Length" => "3"}) + end + } - response = HTTPI.get(request, adapter) - expect(response.body).to eq("basic-auth") + before do + Net::HTTP::Persistent.any_instance.stubs(:start).returns(response) end - it "does not support ntlm authentication" do - request = HTTPI::Request.new(@server.url + "ntlm-auth") - request.auth.ntlm("tester", "vReqSoafRe5O") + describe "settings" do + describe "open_timeout, read_timeout" do + it "are being set on the client" do + request.open_timeout = 30 + request.read_timeout = 40 - expect { HTTPI.get(request, adapter) }. - to raise_error(HTTPI::NotSupportedError, /does not support NTLM authentication/) - end - end - - # it does not support digest auth + adapter.client.expects(:open_timeout=).with(30) + adapter.client.expects(:read_timeout=).with(40) - if RUBY_PLATFORM =~ /java/ - pending "Puma Server complains: SSL not supported on JRuby" - else - context "https requests" do - before :all do - @server = IntegrationServer.run(:ssl => true) + adapter.request(:get) + end end - after :all do - @server.stop - end - - # it does not raise when no certificate was set up - it "works when set up properly" do - request = HTTPI::Request.new(@server.url) - request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file - response = HTTPI.get(request, adapter) - expect(response.body).to eq("get") + describe "write_timeout" do + it "is not supported" do + request.write_timeout = 50 + expect { adapter.request(:get) } + .to raise_error(HTTPI::NotSupportedError, /write_timeout/) + end end end end - end diff --git a/spec/httpi/adapter/net_http_spec.rb b/spec/httpi/adapter/net_http_spec.rb index 51e94e2..346b569 100644 --- a/spec/httpi/adapter/net_http_spec.rb +++ b/spec/httpi/adapter/net_http_spec.rb @@ -1,198 +1,54 @@ require "spec_helper" -require "integration/support/server" +require "httpi/adapter/net_http" +require "httpi/request" -describe HTTPI::Adapter::NetHTTP do +begin + HTTPI::Adapter.load_adapter(:net_http) - subject(:adapter) { :net_http } + describe HTTPI::Adapter::NetHTTP do + let(:adapter) { HTTPI::Adapter::NetHTTP.new(request) } + let(:request) { HTTPI::Request.new("http://example.com") } - context "http requests" do - before :all do - @server = IntegrationServer.run - end - - after :all do - @server.stop - end - - context 'when socks is specified' do - - let(:socks_client) { mock('socks_client') } - let(:request){HTTPI::Request.new(@server.url)} - - it 'uses Net::HTTP.SOCKSProxy as client' do - socks_client.expects(:new).with(URI(@server.url).host, URI(@server.url).port).returns(:socks_client_instance) - Net::HTTP.expects(:SOCKSProxy).with('localhost', 8080).returns socks_client - - request.proxy = 'socks://localhost:8080' - adapter = HTTPI::Adapter::NetHTTP.new(request) - - expect(adapter.client).to eq(:socks_client_instance) + let(:response) { + Object.new.tap do |r| + r.stubs(:code).returns(200) + r.stubs(:body).returns("abc") + r.stubs(:to_hash).returns({"Content-Length" => "3"}) end - end - - it "sends and receives HTTP headers" do - request = HTTPI::Request.new(@server.url + "x-header") - request.headers["X-Header"] = "HTTPI" - - response = HTTPI.get(request, adapter) - expect(response.body).to include("HTTPI") - end + } - it "executes GET requests" do - response = HTTPI.get(@server.url, adapter) - expect(response.body).to eq("get") - expect(response.headers["Content-Type"]).to eq("text/plain") + before do + Net::HTTP.any_instance.stubs(:start).returns(response) end - it "executes POST requests" do - response = HTTPI.post(@server.url, "xml", adapter) - expect(response.body).to eq("post") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - it "executes HEAD requests" do - response = HTTPI.head(@server.url, adapter) - expect(response.code).to eq(200) - expect(response.headers["Content-Type"]).to eq("text/plain") - end + describe "settings" do + describe "open_timeout, read_timeout" do + it "are being set on the client" do + request.open_timeout = 30 + request.read_timeout = 40 - it "executes PUT requests" do - response = HTTPI.put(@server.url, "xml", adapter) - expect(response.body).to eq("put") - expect(response.headers["Content-Type"]).to eq("text/plain") - end + adapter.client.expects(:open_timeout=).with(30) + adapter.client.expects(:read_timeout=).with(40) - it "executes DELETE requests" do - response = HTTPI.delete(@server.url, adapter) - expect(response.body).to eq("delete") - expect(response.headers["Content-Type"]).to eq("text/plain") - end - - context "supports custom methods supported by Net::HTTP" do - let(:request) do - HTTPI::Request.new(@server.url).tap do|r| - r.body = request_body if request_body + adapter.request(:get) end end - let(:request_body) { nil } - - let(:response) { HTTPI.request(http_method, request, adapter) } - - shared_examples_for 'any supported custom method' do - specify { response.body.should eq http_method.to_s } - specify { response.headers["Content-Type"].should eq('text/plain') } - end - - context 'PATCH' do - let(:http_method) { :patch } - let(:request_body) { "xml" } - - it_behaves_like 'any supported custom method' - end - - context 'UNSUPPORTED method' do - let(:http_method) { :unsupported } - - specify { expect { response }.to raise_error HTTPI::NotSupportedError } - end - end - - it "supports basic authentication" do - request = HTTPI::Request.new(@server.url + "basic-auth") - request.auth.basic("admin", "secret") - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("basic-auth") - end - - it "does not support digest authentication" do - request = HTTPI::Request.new(@server.url + "digest-auth") - request.auth.digest("admin", "secret") - - expect { HTTPI.get(request, adapter) }. - to raise_error(HTTPI::NotSupportedError, /does not support HTTP digest authentication/) - end - - it "supports ntlm authentication" do - request = HTTPI::Request.new(@server.url + "ntlm-auth") - request.auth.ntlm("tester", "vReqSoafRe5O") - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("ntlm-auth") - end - - it 'does not support ntlm authentication when Net::NTLM is not available' do - Net.expects(:const_defined?).with(:NTLM).returns false - - request = HTTPI::Request.new(@server.url + 'ntlm-auth') - request.auth.ntlm("testing", "failures") - - expect { HTTPI.get(request, adapter) }. - to raise_error(HTTPI::NotSupportedError, /Net::NTLM is not available/) - end - - it 'does not require ntlm when ntlm authenication is not requested' do - HTTPI::Adapter::NetHTTP.any_instance.stubs(:check_net_ntlm_version!).raises(RuntimeError) - request = HTTPI::Request.new(@server.url) - expect(request.auth.ntlm?).to be false - - # make sure a request doesn't call ntlm check if we don't ask for it. - expect { HTTPI.get(request, adapter) }.not_to raise_error - HTTPI::Adapter::NetHTTP.any_instance.unstub(:check_net_ntlm_version!) - end - - it 'does check ntlm when ntlm authentication is requested' do - request = HTTPI::Request.new(@server.url + "ntlm-auth") - request.auth.ntlm("tester", "vReqSoafRe5O") - - expect { HTTPI.get(request, adapter) }.not_to raise_error - - # the check should also verify that the version of ntlm is supported and still fail if it isn't - HTTPI::Adapter::NetHTTP.any_instance.stubs(:ntlm_version).returns("0.1.1") - - request = HTTPI::Request.new(@server.url + "ntlm-auth") - request.auth.ntlm("tester", "vReqSoafRe5O") - - expect { HTTPI.get(request, adapter) }.to raise_error(ArgumentError, /Invalid version/) - - HTTPI::Adapter::NetHTTP.any_instance.unstub(:ntlm_version) - end - - it "does not crash when authenticate header is missing (on second request)" do - request = HTTPI::Request.new(@server.url + 'ntlm-auth') - request.auth.ntlm("tester", "vReqSoafRe5O") - - expect { HTTPI.get(request, adapter) }. - to_not raise_error - - expect { HTTPI.get(request, adapter) }. - to_not raise_error - end - end - - # it does not support digest auth - - if RUBY_PLATFORM =~ /java/ - pending "Puma Server complains: SSL not supported on JRuby" - else - context "https requests" do - before :all do - @server = IntegrationServer.run(:ssl => true) - end - after :all do - @server.stop - end - - # it does not raise when no certificate was set up - it "works when set up properly" do - request = HTTPI::Request.new(@server.url) - request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file - - response = HTTPI.get(request, adapter) - expect(response.body).to eq("get") + describe "write_timeout" do + if Net::HTTP.method_defined?(:write_timeout=) + it "is being set on the client" do + request.write_timeout = 50 + adapter.client.expects(:write_timeout=).with(50) + adapter.request(:get) + end + else + it "can not be set on the client" do + request.write_timeout = 50 + expect { adapter.request(:get) } + .to raise_error(HTTPI::NotSupportedError, /write_timeout/) + end + end end end end - end diff --git a/spec/integration/curb_spec.rb b/spec/integration/curb_spec.rb index a13af0f..d8f45a7 100644 --- a/spec/integration/curb_spec.rb +++ b/spec/integration/curb_spec.rb @@ -33,6 +33,17 @@ expect(response.headers["Set-Cookie"]).to eq(cookies) end + it "it supports read timeout" do + request = HTTPI::Request.new(@server.url + "timeout") + request.read_timeout = 0.5 # seconds + + expect { HTTPI.get(request, adapter) } + .to raise_error { |error| + expect(error).to be_a(Curl::Err::TimeoutError) + expect(error).to be_a(HTTPI::TimeoutError) + } + end + it "executes GET requests" do response = HTTPI.get(@server.url, adapter) expect(response.body).to eq("get") diff --git a/spec/integration/em_http_spec.rb b/spec/integration/em_http_spec.rb index e980b1e..21be266 100644 --- a/spec/integration/em_http_spec.rb +++ b/spec/integration/em_http_spec.rb @@ -42,6 +42,25 @@ expect(response.headers["Set-Cookie"]).to eq(cookies) end + if RUBY_PLATFORM =~ /java/ + pending <<-MSG + It seems like JRuby is missing support for inactivity timeout! See related issues on GitHub: + - https://github.com/eventmachine/eventmachine/issues/155 + - https://github.com/eventmachine/eventmachine/pull/312 + MSG + else + it "it supports read timeout" do + request = HTTPI::Request.new(@server.url + "timeout") + request.read_timeout = 0.5 # seconds + + expect { HTTPI.get(request, adapter) } + .to raise_error { |error| + expect(error).to be_a(HTTPI::Adapter::EmHttpRequest::EmHttpTimeoutError) + expect(error).to be_a(HTTPI::TimeoutError) + } + end + end + it "executes GET requests" do response = HTTPI.get(@server.url, adapter) expect(response.body).to eq("get") diff --git a/spec/integration/excon_spec.rb b/spec/integration/excon_spec.rb index ed45b58..8d5de2f 100644 --- a/spec/integration/excon_spec.rb +++ b/spec/integration/excon_spec.rb @@ -30,6 +30,17 @@ expect(response.headers["Set-Cookie"]).to eq(cookies) end + it "it supports read timeout" do + request = HTTPI::Request.new(@server.url + "timeout") + request.read_timeout = 0.5 # seconds + + expect { HTTPI.get(request, adapter) } + .to raise_error { |error| + expect(error).to be_a(Excon::Error::Timeout) + expect(error).to be_a(HTTPI::TimeoutError) + } + end + it "executes GET requests" do response = HTTPI.get(@server.url, adapter) expect(response.body).to eq("get") @@ -68,6 +79,21 @@ expect(response.body).to eq("basic-auth") end + it "does not support ntlm authentication" do + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("tester", "vReqSoafRe5O") + + expect { HTTPI.get(request, adapter) }. + to raise_error(HTTPI::NotSupportedError, /does not support NTLM authentication/) + end + + it "supports disabling verify mode" do + request = HTTPI::Request.new(@server.url) + request.auth.ssl.verify_mode = :none + adapter_class = HTTPI::Adapter.load(adapter).new(request) + expect(adapter_class.client.data[:ssl_verify_peer]).to eq(false) + end + it "supports chunked response" do request = HTTPI::Request.new(@server.url) res = "" @@ -102,6 +128,27 @@ response = HTTPI.get(request, adapter) expect(response.body).to eq("get") end + + it "works with client cert and key provided as file path" do + request = HTTPI::Request.new(@server.url) + request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file + request.auth.ssl.cert_file = "spec/fixtures/client_cert.pem" + request.auth.ssl.cert_key_file = "spec/fixtures/client_key.pem" + + response = HTTPI.get(request, adapter) + expect(response.body).to eq("get") + end + + it "works with client cert and key set directly" do + request = HTTPI::Request.new(@server.url) + + request.auth.ssl.ca_cert_file = IntegrationServer.ssl_ca_file + request.auth.ssl.cert = OpenSSL::X509::Certificate.new File.open("spec/fixtures/client_cert.pem").read + request.auth.ssl.cert_key = OpenSSL::PKey.read File.open("spec/fixtures/client_key.pem").read + + response = HTTPI.get(request, adapter) + expect(response.body).to eq("get") + end end end diff --git a/spec/integration/http_spec.rb b/spec/integration/http_spec.rb index 5f167d3..de379c2 100644 --- a/spec/integration/http_spec.rb +++ b/spec/integration/http_spec.rb @@ -30,6 +30,18 @@ expect(response.headers["Set-Cookie"]).to eq(cookies) end + it "it supports read timeout" do + request = HTTPI::Request.new(@server.url + "timeout") + request.read_timeout = 0.5 # seconds + + expect { HTTPI.get(request, adapter) } + .to raise_error { |error| + expect(error).to be_a(HTTP::TimeoutError) + expect(error).to be_a(HTTPI::TimeoutError) + } + end + + it "executes GET requests" do response = HTTPI.get(@server.url, adapter) expect(response.body).to eq("get") @@ -68,6 +80,22 @@ expect(response.body).to eq("basic-auth") end + it "does not support digest authentication" do + request = HTTPI::Request.new(@server.url + "digest-auth") + request.auth.digest("admin", "secret") + + expect { HTTPI.get(request, adapter) }. + to raise_error(HTTPI::NotSupportedError, /does not support HTTP digest authentication/) + end + + it "does not support ntlm authentication" do + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("tester", "vReqSoafRe5O") + + expect { HTTPI.get(request, adapter) }. + to raise_error(HTTPI::NotSupportedError, /does not support NTLM digest authentication/) + end + it "supports chunked response" do skip("Needs investigation") request = HTTPI::Request.new(@server.url) diff --git a/spec/integration/httpclient_spec.rb b/spec/integration/httpclient_spec.rb index 934a060..6e02f5e 100644 --- a/spec/integration/httpclient_spec.rb +++ b/spec/integration/httpclient_spec.rb @@ -30,6 +30,17 @@ expect(response.headers["Set-Cookie"]).to eq(cookies) end + it "it supports read timeout" do + request = HTTPI::Request.new(@server.url + "timeout") + request.read_timeout = 0.5 # seconds + + expect { HTTPI.get(request, adapter) } + .to raise_error { |error| + expect(error).to be_a(HTTPClient::ReceiveTimeoutError) + expect(error).to be_a(HTTPI::TimeoutError) + } + end + it "executes GET requests" do response = HTTPI.get(@server.url, adapter) expect(response.body).to eq("get") diff --git a/spec/integration/net_http_persistent_spec.rb b/spec/integration/net_http_persistent_spec.rb index 28aabbf..81b27f3 100644 --- a/spec/integration/net_http_persistent_spec.rb +++ b/spec/integration/net_http_persistent_spec.rb @@ -30,6 +30,18 @@ expect(response.headers["Set-Cookie"]).to eq(cookies) end + it "it supports read timeout" do + request = HTTPI::Request.new(@server.url + "timeout") + request.read_timeout = 0.5 # seconds + + expect { HTTPI.get(request, adapter) } + .to raise_error { |error| + expect(error.message).to match(/Net::ReadTimeout/) + expect(error).to be_a(Net::HTTP::Persistent::Error) + expect(error).to be_a(HTTPI::TimeoutError) + } + end + it "executes GET requests" do response = HTTPI.get(@server.url, adapter) expect(response.body).to eq("get") @@ -37,8 +49,8 @@ end it "executes POST requests" do - request = HTTPI::Request.new(url: @server.url, open_timeout: 1, read_timeout: 1, body: "xml") - + request = HTTPI::Request.new(url: @server.url, body: "xml") + response = HTTPI.post(request, adapter) expect(response.body).to eq("post") expect(response.headers["Content-Type"]).to eq("text/plain") @@ -70,6 +82,14 @@ expect(response.body).to eq("basic-auth") end + it "does not support ntlm authentication" do + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("tester", "vReqSoafRe5O") + + expect { HTTPI.get(request, adapter) }. + to raise_error(HTTPI::NotSupportedError, /does not support NTLM authentication/) + end + # it does not support digest authentication it "supports chunked response" do diff --git a/spec/integration/net_http_spec.rb b/spec/integration/net_http_spec.rb index fcee178..04b073c 100644 --- a/spec/integration/net_http_spec.rb +++ b/spec/integration/net_http_spec.rb @@ -14,6 +14,21 @@ @server.stop end + context "when socks is specified" do + let(:socks_client) { mock("socks_client") } + let(:request) { HTTPI::Request.new(@server.url) } + + it "uses Net::HTTP.SOCKSProxy as client" do + socks_client.expects(:new).with(URI(@server.url).host, URI(@server.url).port).returns(:socks_client_instance) + Net::HTTP.expects(:SOCKSProxy).with("localhost", 8080).returns socks_client + + request.proxy = "socks://localhost:8080" + adapter = HTTPI::Adapter::NetHTTP.new(request) + + expect(adapter.client).to eq(:socks_client_instance) + end + end + it "sends and receives HTTP headers" do request = HTTPI::Request.new(@server.url + "x-header") request.headers["X-Header"] = "HTTPI" @@ -30,6 +45,17 @@ expect(response.headers["Set-Cookie"]).to eq(cookies) end + it "it supports read timeout" do + request = HTTPI::Request.new(@server.url + "timeout") + request.read_timeout = 0.5 # seconds + + expect { HTTPI.get(request, adapter) } + .to raise_error { |error| + expect(error).to be_a(Net::ReadTimeout) + expect(error).to be_a(HTTPI::TimeoutError) + } + end + it "executes GET requests" do response = HTTPI.get(@server.url, adapter) expect(response.body).to eq("get") @@ -60,6 +86,34 @@ expect(response.headers["Content-Type"]).to eq("text/plain") end + context "custom methods" do + let(:request) { + HTTPI::Request.new(@server.url).tap do |r| + r.body = request_body if request_body + end + } + let(:request_body) { nil } + let(:response) { HTTPI.request(http_method, request, adapter) } + + shared_examples_for "any supported custom method" do + specify { response.body.should eq http_method.to_s } + specify { response.headers["Content-Type"].should eq("text/plain") } + end + + context "PATCH method" do + let(:http_method) { :patch } + let(:request_body) { "xml" } + + it_behaves_like "any supported custom method" + end + + context "UNSUPPORTED method" do + let(:http_method) { :unsupported } + + specify { expect { response }.to raise_error HTTPI::NotSupportedError } + end + end + it "supports basic authentication" do request = HTTPI::Request.new(@server.url + "basic-auth") request.auth.basic("admin", "secret") @@ -68,7 +122,69 @@ expect(response.body).to eq("basic-auth") end - # it does not support digest authentication + it "does not support digest authentication" do + request = HTTPI::Request.new(@server.url + "digest-auth") + request.auth.digest("admin", "secret") + + expect { HTTPI.get(request, adapter) }. + to raise_error(HTTPI::NotSupportedError, /does not support HTTP digest authentication/) + end + + it "supports ntlm authentication" do + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("tester", "vReqSoafRe5O") + + response = HTTPI.get(request, adapter) + expect(response.body).to eq("ntlm-auth") + end + + it "does not support ntlm authentication when Net::NTLM is not available" do + Net.expects(:const_defined?).with(:NTLM).returns false + + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("testing", "failures") + + expect { HTTPI.get(request, adapter) }. + to raise_error(HTTPI::NotSupportedError, /Net::NTLM is not available/) + end + + it "does not require ntlm when ntlm authenication is not requested" do + HTTPI::Adapter::NetHTTP.any_instance.stubs(:check_net_ntlm_version!).raises(RuntimeError) + request = HTTPI::Request.new(@server.url) + expect(request.auth.ntlm?).to be false + + # make sure a request doesn't call ntlm check if we don't ask for it. + expect { HTTPI.get(request, adapter) }.not_to raise_error + HTTPI::Adapter::NetHTTP.any_instance.unstub(:check_net_ntlm_version!) + end + + it "does check ntlm when ntlm authentication is requested" do + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("tester", "vReqSoafRe5O") + + expect { HTTPI.get(request, adapter) }.not_to raise_error + + # the check should also verify that the version of ntlm is supported and still fail if it isn't + HTTPI::Adapter::NetHTTP.any_instance.stubs(:ntlm_version).returns("0.1.1") + + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("tester", "vReqSoafRe5O") + + expect { HTTPI.get(request, adapter) }.to raise_error(ArgumentError, /Invalid version/) + + HTTPI::Adapter::NetHTTP.any_instance.unstub(:ntlm_version) + end + + it "does not crash when authenticate header is missing (on second request)" do + request = HTTPI::Request.new(@server.url + "ntlm-auth") + request.auth.ntlm("tester", "vReqSoafRe5O") + + expect { HTTPI.get(request, adapter) }. + to_not raise_error + + expect { HTTPI.get(request, adapter) }. + to_not raise_error + end it "supports chunked response" do request = HTTPI::Request.new(@server.url) diff --git a/spec/integration/support/application.rb b/spec/integration/support/application.rb index 20c9bc4..0f6d26c 100644 --- a/spec/integration/support/application.rb +++ b/spec/integration/support/application.rb @@ -15,9 +15,10 @@ def self.respond_with(body) } end - map "/repeat" do + map "/timeout" do run lambda { |env| - IntegrationServer.respond_with :body => env["rack.input"].read + sleep 2 + IntegrationServer.respond_with "done" } end