Browse files

Add keep-alive support

  • Loading branch information...
1 parent 7ca1f63 commit 0bd7117e4c4d3981471523be924495df80e5571b @byroot byroot committed Mar 22, 2013
Showing with 225 additions and 92 deletions.
  1. +38 −0 Gemfile.lock
  2. +4 −1 lib/restclient.rb
  3. +18 −20 lib/restclient/request.rb
  4. +1 −0 rest-client.gemspec
  5. +57 −27 spec/integration/request_spec.rb
  6. +107 −44 spec/request_spec.rb
View
38 Gemfile.lock
@@ -0,0 +1,38 @@
+PATH
+ remote: .
+ specs:
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ net-http-persistent
+ netrc (~> 0.7.7)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.3.3)
+ crack (0.3.2)
+ diff-lcs (1.2.1)
+ mime-types (1.21)
+ net-http-persistent (2.8)
+ netrc (0.7.7)
+ rake (10.0.3)
+ rspec (2.13.0)
+ rspec-core (~> 2.13.0)
+ rspec-expectations (~> 2.13.0)
+ rspec-mocks (~> 2.13.0)
+ rspec-core (2.13.1)
+ rspec-expectations (2.13.0)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.13.0)
+ webmock (1.11.0)
+ addressable (>= 2.2.7)
+ crack (>= 0.3.2)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ rake
+ rest-client!
+ rspec (>= 2.0)
+ webmock (>= 0.9.1)
View
5 lib/restclient.rb
@@ -1,6 +1,7 @@
require 'uri'
require 'zlib'
require 'stringio'
+require 'net/http/persistent'
begin
require 'net/https'
@@ -93,9 +94,11 @@ def self.options(url, headers={}, &block)
end
class << self
- attr_accessor :proxy
+ attr_accessor :proxy, :persistent
end
+ @persistent = true
+
# Setup the log for RestClient calls.
# Value should be a logger but can can be stdout, stderr, or a filename.
# You can also configure logging by the environment variable RESTCLIENT_LOG.
View
38 lib/restclient/request.rb
@@ -30,7 +30,7 @@ class Request
:payload, :user, :password, :timeout, :max_redirects,
:open_timeout, :raw_response, :verify_ssl, :ssl_client_cert,
:ssl_client_key, :ssl_ca_file, :processed_headers, :args,
- :ssl_version
+ :ssl_version, :cert_store, :persistent
def self.execute(args, & block)
new(args).execute(& block)
@@ -57,6 +57,8 @@ def initialize args
@ssl_client_key = args[:ssl_client_key] || nil
@ssl_ca_file = args[:ssl_ca_file] || nil
@ssl_version = args[:ssl_version] || 'SSLv3'
+ @cert_store = args[:cert_store]
+ @persistent = args.has_key?(:persistent) ? args[:persistent] : RestClient.persistent
@tf = nil # If you are a raw request, this is your tempfile
@max_redirects = args[:max_redirects] || 10
@processed_headers = make_headers headers
@@ -98,15 +100,6 @@ def make_headers user_headers
headers
end
- def net_http_class
- if RestClient.proxy
- proxy_uri = URI.parse(RestClient.proxy)
- Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
- else
- Net::HTTP
- end
- end
-
def net_http_request_class(method)
Net::HTTP.const_get(method.to_s.capitalize)
end
@@ -146,8 +139,11 @@ def process_payload(p=nil, parent_key=nil)
def transmit uri, req, payload, & block
setup_credentials req
- net = net_http_class.new(uri.host, uri.port)
- net.use_ssl = uri.is_a?(URI::HTTPS)
+ proxy = URI.parse(RestClient.proxy) if RestClient.proxy
+ app_name = 'rest-client'
+ app_name += '-persistent' if @persistent
+ net = Net::HTTP::Persistent.new(app_name, proxy)
+
net.ssl_version = @ssl_version
err_msg = nil
if (@verify_ssl == false) || (@verify_ssl == OpenSSL::SSL::VERIFY_NONE)
@@ -163,6 +159,7 @@ def transmit uri, req, payload, & block
end
end
net.cert = @ssl_client_cert if @ssl_client_cert
+ net.cert_store = @cert_store if @cert_store
net.key = @ssl_client_key if @ssl_client_key
net.ca_file = @ssl_ca_file if @ssl_ca_file
net.read_timeout = @timeout if @timeout
@@ -178,14 +175,13 @@ def transmit uri, req, payload, & block
log_request
- net.start do |http|
- if @block_response
- http.request(req, payload ? payload.to_s : nil, & @block_response)
- else
- res = http.request(req, payload ? payload.to_s : nil) { |http_response| fetch_body(http_response) }
- log_response res
- process_result res, & block
- end
+ req.body = payload.to_s if payload
+ if @block_response
+ net.request(uri, req, & @block_response)
+ else
+ res = net.request(uri, req) { |http_response| fetch_body(http_response) }
+ log_response res
+ process_result res, & block
end
rescue OpenSSL::SSL::SSLError => e
if err_msg
@@ -197,6 +193,8 @@ def transmit uri, req, payload, & block
raise RestClient::ServerBrokeConnection
rescue Timeout::Error
raise RestClient::RequestTimeout
+ ensure
+ net.shutdown unless @persistent
end
def setup_credentials(req)
View
1 rest-client.gemspec
@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
s.summary = 'Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions.'
s.add_runtime_dependency(%q<mime-types>, [">= 1.16"])
+ s.add_runtime_dependency(%q<net-http-persistent>)
s.add_development_dependency(%q<webmock>, [">= 0.9.1"])
s.add_development_dependency(%q<rspec>, [">= 2.0"])
s.add_dependency(%q<netrc>, ["~> 0.7.7"])
View
84 spec/integration/request_spec.rb
@@ -1,35 +1,65 @@
require File.join( File.dirname(File.expand_path(__FILE__)), '../base')
describe RestClient::Request do
- describe "ssl verification" do
- it "is successful with the correct ca_file" do
- request = RestClient::Request.new(
- :method => :get,
- :url => 'https://www.mozilla.com',
- :verify_ssl => OpenSSL::SSL::VERIFY_PEER,
- :ssl_ca_file => File.join(File.dirname(__FILE__), "certs", "equifax.crt")
- )
- expect { request.execute }.to_not raise_error
+ shared_examples 'an http client' do
+ describe "ssl verification" do
+ it "is successful with the correct ca_file" do
+ request = RestClient::Request.new(
+ :method => :get,
+ :url => 'https://www.mozilla.com',
+ :verify_ssl => OpenSSL::SSL::VERIFY_PEER,
+ :ssl_ca_file => File.join(File.dirname(__FILE__), "certs", "equifax.crt")
+ )
+ expect { request.execute }.to_not raise_error
+ end
+
+ # I don' think this feature is useful anymore (under 1.9.3 at the very least).
+ #
+ # Exceptions in verify_callback are ignored; RestClient has to catch OpenSSL::SSL::SSLError
+ # and either re-throw it as is, or throw SSLCertificateNotVerified
+ # based on the contents of the message field of the original exception
+ #.
+ # The client has to handle OpenSSL::SSL::SSLError exceptions anyway,
+ # why make them handle both OpenSSL *AND* RestClient exceptions???
+ #
+ # also see https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl.c#L237
+ it "is unsuccessful with an incorrect ca_file" do
+ request = RestClient::Request.new(
+ :method => :get,
+ :url => 'https://www.mozilla.com',
+ :verify_ssl => OpenSSL::SSL::VERIFY_PEER,
+ :ssl_ca_file => File.join(File.dirname(__FILE__), "certs", "verisign.crt"),
+ :cert_store => OpenSSL::X509::Store.new
+ )
+ expect { request.execute }.to raise_error(RestClient::SSLCertificateNotVerified)
+ end
+ end
+ end
+
+ describe 'with keep alive' do
+ before :each do
+ @previous_persistent = RestClient.persistent
+ RestClient.persistent = true
+ end
+
+ after :each do
+ RestClient.persistent = @previous_persistent
end
- # I don' think this feature is useful anymore (under 1.9.3 at the very least).
- #
- # Exceptions in verify_callback are ignored; RestClient has to catch OpenSSL::SSL::SSLError
- # and either re-throw it as is, or throw SSLCertificateNotVerified
- # based on the contents of the message field of the original exception
- #.
- # The client has to handle OpenSSL::SSL::SSLError exceptions anyway,
- # why make them handle both OpenSSL *AND* RestClient exceptions???
- #
- # also see https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl.c#L237
- it "is unsuccessful with an incorrect ca_file" do
- request = RestClient::Request.new(
- :method => :get,
- :url => 'https://www.mozilla.com',
- :verify_ssl => OpenSSL::SSL::VERIFY_PEER,
- :ssl_ca_file => File.join(File.dirname(__FILE__), "certs", "verisign.crt")
- )
- expect { request.execute }.to raise_error(RestClient::SSLCertificateNotVerified)
+ it_should_behave_like 'an http client'
+ end
+
+ describe 'without keep alive' do
+ before :each do
+ @previous_persistent = RestClient.persistent
+ RestClient.persistent = false
+ end
+
+ after :each do
+ RestClient.persistent = @previous_persistent
end
+
+ it_should_behave_like 'an http client'
end
+
end
View
151 spec/request_spec.rb
@@ -11,14 +11,19 @@
@uri.stub!(:request_uri).and_return('/resource')
@uri.stub!(:host).and_return('some')
@uri.stub!(:port).and_return(80)
+ @uri.stub!(:scheme).and_return('http')
+ @uri.stub!(:body=)
- @net = mock("net::http base")
- @http = mock("net::http connection")
- Net::HTTP.stub!(:new).and_return(@net)
- @net.stub!(:start).and_yield(@http)
- @net.stub!(:use_ssl=)
+ @net = mock("net::http::persistent")
+ Net::HTTP::Persistent.stub!(:new).and_return(@net)
@net.stub!(:verify_mode=)
+ @net.stub!(:ssl_version=)
+ @net.stub!(:shutdown)
RestClient.log = nil
+
+ @req = mock("net::http::request")
+ @req.stub!(:body=)
+
end
it "accept */* mimetype, preferring xml" do
@@ -204,19 +209,19 @@
end
it "transmits the request with Net::HTTP" do
- @http.should_receive(:request).with('req', 'payload')
+ @net.should_receive(:request).with(@uri, @req)
@net.should_receive(:ssl_version=).with('SSLv3')
@request.should_receive(:process_result)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
describe "payload" do
it "sends nil payloads" do
- @http.should_receive(:request).with('req', nil)
+ @net.should_receive(:request).with(@uri, @req)
@net.should_receive(:ssl_version=).with('SSLv3')
@request.should_receive(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', nil)
+ @request.transmit(@uri, @req, nil)
end
it "passes non-hash payloads straight through" do
@@ -242,17 +247,17 @@
describe "credentials" do
it "sets up the credentials prior to the request" do
- @http.stub!(:request)
+ @net.stub!(:request)
@net.should_receive(:ssl_version=).with('SSLv3')
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.stub!(:user).and_return('joe')
@request.stub!(:password).and_return('mypass')
- @request.should_receive(:setup_credentials).with('req')
+ @request.should_receive(:setup_credentials).with(@req)
- @request.transmit(@uri, 'req', nil)
+ @request.transmit(@uri, @req, nil)
end
it "does not attempt to send any credentials if user is nil" do
@@ -272,9 +277,9 @@
end
it "catches EOFError and shows the more informative ServerBrokeConnection" do
- @http.stub!(:request).and_raise(EOFError)
+ @net.stub!(:request).and_raise(EOFError)
@net.should_receive(:ssl_version=).with('SSLv3')
- lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection)
+ lambda { @request.transmit(@uri, @req, nil) }.should raise_error(RestClient::ServerBrokeConnection)
end
it "class method execute wraps constructor" do
@@ -309,16 +314,76 @@
end
describe "proxy" do
+ before :each do
+ @response = Net::HTTPNoContent.new("", "204", "No Content")
+ @response.stub!(:read_body).and_return(nil)
+ @net.stub!(:request).and_return(@response)
+ end
+
it "creates a proxy class if a proxy url is given" do
RestClient.stub!(:proxy).and_return("http://example.com/")
- @request.net_http_class.proxy_class?.should be_true
+ Net::HTTP::Persistent.should_receive(:new).with(instance_of(String), URI.parse("http://example.com/"))
+
+ @request.transmit(@req, @uri, 'payload')
end
it "creates a non-proxy class if a proxy url is not given" do
- @request.net_http_class.proxy_class?.should be_false
+ Net::HTTP::Persistent.should_receive(:new).with(instance_of(String), nil)
+ @request.transmit(@req, @uri, 'payload')
end
end
+ describe "connection pool" do
+ before :each do
+ @response = Net::HTTPNoContent.new("", "204", "No Content")
+ @response.stub!(:read_body).and_return(nil)
+ @net.stub!(:request).and_return(@response)
+ end
+
+ describe "when persistent connections are disabled" do
+ before :each do
+ @previous_persistent = RestClient.persistent
+ RestClient.persistent = false
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+ end
+
+ after :each do
+ RestClient.persistent = @previous_persistent
+ end
+
+ it "use 'rest-client' pool" do
+ Net::HTTP::Persistent.should_receive(:new).with('rest-client', nil)
+ @request.transmit(@req, @uri, 'payload')
+ end
+
+ it "automatically close connections" do
+ @net.should_receive(:shutdown)
+ @request.transmit(@req, @uri, 'payload')
+ end
+ end
+
+ describe "when persistent connections are enabled" do
+ before :each do
+ @previous_persistent = RestClient.persistent
+ RestClient.persistent = true
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+ end
+
+ after :each do
+ RestClient.persistent = @previous_persistent
+ end
+
+ it "use 'rest-client' pool" do
+ Net::HTTP::Persistent.should_receive(:new).with('rest-client-persistent', nil)
+ @request.transmit(@req, @uri, 'payload')
+ end
+
+ it "do not close connections" do
+ @net.should_not_receive(:shutdown)
+ @request.transmit(@req, @uri, 'payload')
+ end
+ end
+ end
describe "logging" do
it "logs a get request" do
@@ -381,38 +446,36 @@
describe "timeout" do
it "set read_timeout" do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123, :ssl_version => 'SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@net.should_receive(:read_timeout=).with(123)
@net.should_receive(:ssl_version=).with('SSLv3')
- @request.transmit(@uri, 'req', nil)
+ @request.transmit(@uri, @req, nil)
end
it "set open_timeout" do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :open_timeout => 123, :ssl_version => 'SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@net.should_receive(:open_timeout=).with(123)
@net.should_receive(:ssl_version=).with('SSLv3')
- @request.transmit(@uri, 'req', nil)
+ @request.transmit(@uri, @req, nil)
end
end
describe "ssl" do
it "uses SSL when the URI refers to a https address" do
- @uri.stub!(:is_a?).with(URI::HTTPS).and_return(true)
- @net.should_receive(:use_ssl=).with(true)
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should default to not verifying ssl certificates" do
@@ -422,20 +485,20 @@
it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do
@net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do
@request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true, :ssl_version => 'SSLv3')
@net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should set net.verify_mode to the passed value if verify_ssl is an OpenSSL constant" do
@@ -448,10 +511,10 @@
@net.should_receive(:verify_mode=).with(mode)
@net.should_receive(:verify_callback=)
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should default to not having an ssl_client_cert" do
@@ -468,10 +531,10 @@
)
@net.should_receive(:cert=).with("whatsupdoc!")
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should not set the ssl_client_cert if it is not provided" do
@@ -483,10 +546,10 @@
)
@net.should_not_receive(:cert=).with("whatsupdoc!")
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should default to not having an ssl_client_key" do
@@ -503,10 +566,10 @@
)
@net.should_receive(:key=).with("whatsupdoc!")
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should not set the ssl_client_key if it is not provided" do
@@ -518,10 +581,10 @@
)
@net.should_not_receive(:key=).with("whatsupdoc!")
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should default to not having an ssl_ca_file" do
@@ -538,10 +601,10 @@
)
@net.should_receive(:ca_file=).with("Certificate Authority File")
@net.should_receive(:ssl_version=).with('SSLv3')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
it "should not set the ssl_ca_file if it is not provided" do
@@ -553,10 +616,10 @@
)
@net.should_not_receive(:ca_file=).with("Certificate Authority File")
@net.should_receive(:ssl_version=).with('TSLv1')
- @http.stub!(:request)
+ @net.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ @request.transmit(@uri, @req, 'payload')
end
end
@@ -569,9 +632,9 @@
)
net_http_res = Net::HTTPNoContent.new("", "204", "No Content")
net_http_res.stub!(:read_body).and_return(nil)
- @http.should_receive(:request).and_return(@request.fetch_body(net_http_res))
+ @net.should_receive(:request).and_return(@request.fetch_body(net_http_res))
@net.should_receive(:ssl_version=).with('SSLv3')
- response = @request.transmit(@uri, 'req', 'payload')
+ response = @request.transmit(@uri, @req, 'payload')
response.should_not be_nil
response.code.should == 204
end

0 comments on commit 0bd7117

Please sign in to comment.