From e8481dd14dc2af0667866a319832dc9adc8d54bf Mon Sep 17 00:00:00 2001 From: Steve Bazyl Date: Fri, 18 Dec 2015 14:37:21 -0800 Subject: [PATCH] Use HTTPClient instead of Net::HTTP --- google-api-client.gemspec | 1 + lib/google/apis/core/base_service.rb | 2 + lib/google/apis/core/http_client_adapter.rb | 82 +++++++++++++++++++++ lib/google/apis/options.rb | 6 +- spec/google/apis/options_spec.rb | 8 -- spec/integration_tests/adsense_spec.rb | 1 + spec/integration_tests/pubsub_spec.rb | 6 +- spec/spec_helper.rb | 34 ++++++++- 8 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 lib/google/apis/core/http_client_adapter.rb diff --git a/google-api-client.gemspec b/google-api-client.gemspec index 37a83fdaf..ae510cd1e 100644 --- a/google-api-client.gemspec +++ b/google-api-client.gemspec @@ -26,5 +26,6 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'hurley', '~> 0.1' spec.add_runtime_dependency 'googleauth', '~> 0.5' spec.add_runtime_dependency 'thor', '~> 0.19' + spec.add_runtime_dependency 'httpclient', '~> 2.7' spec.add_runtime_dependency 'memoist', '~> 0.11' end diff --git a/lib/google/apis/core/base_service.rb b/lib/google/apis/core/base_service.rb index c3431f31d..79c631c7a 100644 --- a/lib/google/apis/core/base_service.rb +++ b/lib/google/apis/core/base_service.rb @@ -19,6 +19,7 @@ require 'google/apis/core/api_command' require 'google/apis/core/batch' require 'google/apis/core/upload' require 'google/apis/core/download' +require 'google/apis/core/http_client_adapter' require 'google/apis/options' require 'googleauth' require 'hurley' @@ -291,6 +292,7 @@ module Google # @return [Hurley::Client] def new_client client = Hurley::Client.new + client.connection = Google::Apis::Core::HttpClientAdapter.new unless client_options.use_net_http client.request_options.timeout = request_options.timeout_sec client.request_options.open_timeout = request_options.open_timeout_sec client.request_options.proxy = client_options.proxy_url diff --git a/lib/google/apis/core/http_client_adapter.rb b/lib/google/apis/core/http_client_adapter.rb new file mode 100644 index 000000000..a58fd4b11 --- /dev/null +++ b/lib/google/apis/core/http_client_adapter.rb @@ -0,0 +1,82 @@ +require 'httpclient' +require 'hurley' +require 'hurley/client' + +module Google + module Apis + module Core + # HTTPClient adapter for Hurley. + class HttpClientAdapter + + def call(request) + client = ::HTTPClient.new + configure_client(client, request) + + begin + ::Hurley::Response.new(request) do |res| + http_res = client.request(request.verb.to_s.upcase, request.url.to_s, nil, request.body_io, request.header.to_hash, false) do |http_res, chunk| + copy_response(http_res, res) + res.receive_body(chunk) + end + copy_response(http_res, res) + end + rescue ::HTTPClient::TimeoutError, Errno::ETIMEDOUT + raise ::Hurley::Timeout, $! + rescue ::HTTPClient::BadResponseError => err + if err.message.include?('status 407') + raise ::Hurley::ConnectionFailed, %{407 "Proxy Authentication Required "} + else + raise Hurley::ClientError, $! + end + rescue Errno::ECONNREFUSED, EOFError + raise ::Hurley::ConnectionFailed, $! + rescue => err + if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err + raise Hurley::SSLError, err + else + raise + end + end + end + + def copy_response(http_res, res) + unless res.status_code + res.status_code = http_res.status.to_i + http_res.header.all.each do |(k,v)| + res.header[k] = v + end + end + end + + def configure_client(client, request) + client.transparent_gzip_decompression = true + if request.options.proxy + proxy = request.options.proxy + client.proxy = sprintf('%s:%d', proxy.host, proxy.port) + if proxy.user && proxy.password + client.set_proxy_auth proxy.user, proxy.password + end + end + if request.options.timeout + client.connect_timeout = request.options.timeout + client.receive_timeout = request.options.timeout + client.send_timeout = request.options.timeout + end + if request.options.open_timeout + client.connect_timeout = request.options.open_timeout + client.send_timeout = request.options.open_timeout + end + ssl_config = client.ssl_config + ssl_opts = request.ssl_options + ssl_config.verify_mode = ssl_opts.openssl_verify_mode + ssl_config.cert_store = ssl_opts.openssl_cert_store + ssl_config.add_trust_ca ssl_opts.ca_file if ssl_opts.ca_file + ssl_config.add_trust_ca ssl_opts.ca_path if ssl_opts.ca_path + ssl_config.client_cert = ssl_opts.openssl_client_cert if ssl_opts.openssl_client_cert + ssl_config.client_key = ssl_opts.openssl_client_key if ssl_opts.openssl_client_key + ssl_config.verify_depth = ssl_opts.verify_depth if ssl_opts.verify_depth + end + end + end + end +end diff --git a/lib/google/apis/options.rb b/lib/google/apis/options.rb index 593cb58ca..f790e0cd1 100644 --- a/lib/google/apis/options.rb +++ b/lib/google/apis/options.rb @@ -18,7 +18,8 @@ module Google ClientOptions = Struct.new( :application_name, :application_version, - :proxy_url) + :proxy_url, + :use_net_http) RequestOptions = Struct.new( :authorization, @@ -73,7 +74,8 @@ module Google new_options end end - + + ClientOptions.default.use_net_http = false ClientOptions.default.application_name = 'unknown' ClientOptions.default.application_version = '0.0.0' diff --git a/spec/google/apis/options_spec.rb b/spec/google/apis/options_spec.rb index fa5a4f46e..387ecb560 100644 --- a/spec/google/apis/options_spec.rb +++ b/spec/google/apis/options_spec.rb @@ -37,12 +37,4 @@ RSpec.describe Google::Apis::RequestOptions do it 'should allow nil in merge' do expect(options.merge(nil)).to be_an_instance_of(Google::Apis::RequestOptions) end - - it 'should override default options' do - Google::Apis::RequestOptions.default.header = 'Content-Length: 50' - opts = Google::Apis::RequestOptions.new - opts.header = 'Content-Length: 70' - expect(options.merge(opts).header).to eq 'Content-Length: 70' - end - end diff --git a/spec/integration_tests/adsense_spec.rb b/spec/integration_tests/adsense_spec.rb index 2a56a3d1a..2033aca20 100644 --- a/spec/integration_tests/adsense_spec.rb +++ b/spec/integration_tests/adsense_spec.rb @@ -15,6 +15,7 @@ RSpec.describe Google::Apis::AdsenseV1_4, :if => run_integration_tests? do end it 'should download a report with multiple dimensions' do + pending "Not enabled for test account" report = @adsense.generate_report( Date.today.to_s, Date.today.to_s, dimension: ["DATE", "AD_UNIT_NAME"] ) report_header_names = report.headers.map { |h| h.name } diff --git a/spec/integration_tests/pubsub_spec.rb b/spec/integration_tests/pubsub_spec.rb index dabb16a1a..275913803 100644 --- a/spec/integration_tests/pubsub_spec.rb +++ b/spec/integration_tests/pubsub_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -require 'google/apis/pubsub_v1beta2' +require 'google/apis/pubsub_v1' require 'googleauth' -Pubsub = Google::Apis::PubsubV1beta2 +Pubsub = Google::Apis::PubsubV1 -RSpec.describe Google::Apis::PubsubV1beta2, :if => run_integration_tests? do +RSpec.describe Google::Apis::PubsubV1, :if => run_integration_tests? do before(:context) do WebMock.allow_net_connect! diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c5ce71461..bb85032d5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -87,11 +87,12 @@ end # Enable retries for tests Google::Apis::RequestOptions.default.retries = 5 +# Allow testing different adapters +Google::Apis::ClientOptions.default.use_net_http = true if ENV['USE_NET_HTTP'] # Temporarily patch WebMock to allow chunked responses for Net::HTTP module Net module WebMockHTTPResponse def eval_chunk(chunk) - puts chunk.is_a? Exception chunk if chunk.is_a?(String) chunk.read if chunk.is_a?(IO) chunk.call if chunk.is_a?(Proc) @@ -121,6 +122,37 @@ module Net end end +class WebMockHTTPClient + def eval_chunk(chunk) + chunk if chunk.is_a?(String) + chunk.read if chunk.is_a?(IO) + chunk.call if chunk.is_a?(Proc) + fail HTTPClient::TimeoutError if chunk == ::Timeout::Error + fail chunk if chunk.is_a?(Class) + chunk + end + + def build_httpclient_response(webmock_response, stream = false, req_header = nil, &block) + body = stream ? StringIO.new(webmock_response.body) : webmock_response.body + response = HTTP::Message.new_response(body, req_header) + response.header.init_response(webmock_response.status[0]) + response.reason = webmock_response.status[1] + webmock_response.headers.to_a.each { |name, value| response.header.set(name, value) } + + raise HTTPClient::TimeoutError if webmock_response.should_timeout + webmock_response.raise_error_if_any + + body_parts = Array(webmock_response.body) + body_parts.each do |chunk| + chunk = eval_chunk(chunk) + block.call(response, chunk) if block + end + + response + end +end + + def run_integration_tests? ENV['GOOGLE_APPLICATION_CREDENTIALS'] && ENV['GOOGLE_PROJECT_ID'] end