Skip to content

Commit cdf6281

Browse files
authored
feat!: Replace underlying httpclient library with Faraday (googleapis#23524)
Release-As: 1.0.0
1 parent 316ac75 commit cdf6281

File tree

13 files changed

+221
-173
lines changed

13 files changed

+221
-173
lines changed

google-apis-core/OVERVIEW.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ This library includes common base classes and dependencies used by legacy REST
44
clients for Google APIs. It is used by client libraries, but you should not
55
need to install it by itself.
66

7+
## Usage
8+
9+
In most cases, this library is installed automatically as a dependency of
10+
another library. For example, if you install the
11+
[google-apis-drive_v3](https://rubygems.org/gems/google-apis-drive_v3) client
12+
library, it will bring in the latest `google-apis-core` as a dependency. Thus,
13+
in most cases, you do not need to add `google-apis-core` to your Gemfile
14+
directly.
15+
16+
Earlier (0.x) versions of this library utilized the legacy
17+
[httpclient](https://rubygems.org/gems/httpclient) gem and made some of its
18+
interfaces available for advanced use cases. Version 1.0 and later of this
19+
library replaced httpclient with [faraday](https://rubygems.org/gems/faraday).
20+
If your application makes use of the httpclient interfaces (this is rare), you
21+
should pin `google-apis-core` to a 0.x version in your Gemfile. For example:
22+
23+
gem "google-apis-core", "~> 0.18"
24+
725
## Documentation
826

927
More detailed descriptions of the Google legacy REST clients are available in two documents.

google-apis-core/google-apis-core.gemspec

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ Gem::Specification.new do |gem|
2121

2222
gem.required_ruby_version = '>= 3.1'
2323
gem.add_runtime_dependency "representable", "~> 3.0"
24-
gem.add_runtime_dependency "retriable", ">= 2.0", "< 4.a"
25-
gem.add_runtime_dependency "addressable", "~> 2.5", ">= 2.5.1"
26-
gem.add_runtime_dependency "mini_mime", "~> 1.0"
27-
gem.add_runtime_dependency "googleauth", "~> 1.9"
28-
gem.add_runtime_dependency "httpclient", ">= 2.8.3", "< 3.a"
29-
gem.add_runtime_dependency "mutex_m" # used by httpclient
24+
gem.add_runtime_dependency "retriable", "~> 3.1"
25+
gem.add_runtime_dependency "addressable", "~> 2.8", ">= 2.8.7"
26+
gem.add_runtime_dependency "mini_mime", "~> 1.1"
27+
gem.add_runtime_dependency "googleauth", "~> 1.14"
28+
gem.add_runtime_dependency "faraday", "~> 2.13"
29+
gem.add_runtime_dependency "faraday-follow_redirects", "~> 0.3"
3030
end

google-apis-core/lib/google/apis/core/base_service.rb

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
require 'google/apis/core/version'
1919
require 'google/apis/core/api_command'
2020
require 'google/apis/core/batch'
21+
require 'google/apis/core/faraday_integration'
2122
require 'google/apis/core/upload'
2223
require 'google/apis/core/storage_upload'
2324
require 'google/apis/core/download'
2425
require 'google/apis/core/storage_download'
2526
require 'google/apis/options'
2627
require 'googleauth'
27-
require 'httpclient'
2828

2929
module Google
3030
module Apis
@@ -149,8 +149,8 @@ def root_url= url_or_template
149149
# @return [Addressable::URI]
150150
attr_accessor :batch_path
151151

152-
# HTTP client
153-
# @return [HTTPClient]
152+
# Faraday HTTP connection
153+
# @return [Faraday::Connection]
154154
attr_writer :client
155155

156156
# General settings
@@ -263,8 +263,8 @@ def batch_upload(options = nil)
263263
batch_command.execute(client)
264264
end
265265

266-
# Get the current HTTP client
267-
# @return [HTTPClient]
266+
# Get the current HTTP connection
267+
# @return [Faraday::Connection]
268268
def client
269269
@client ||= new_client
270270
end
@@ -542,39 +542,22 @@ def end_batch
542542
Thread.current[:google_api_batch_service] = nil
543543
end
544544

545-
# Create a new HTTP client
546-
# @return [HTTPClient]
545+
# Create a new HTTP connection
546+
# @return [Faraday::Connection]
547547
def new_client
548-
client = ::HTTPClient.new
549-
550-
if client_options.transparent_gzip_decompression
551-
client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
552-
end
553-
554-
client.proxy = client_options.proxy_url if client_options.proxy_url
555-
556-
if client_options.open_timeout_sec
557-
client.connect_timeout = client_options.open_timeout_sec
558-
end
559-
560-
if client_options.read_timeout_sec
561-
client.receive_timeout = client_options.read_timeout_sec
548+
options = {}
549+
request_options = {params_encoder: Faraday::FlatParamsEncoder}
550+
options[:proxy] = {uri: client_options.proxy_url} if client_options.proxy_url
551+
request_options[:open_timeout] = client_options.open_timeout_sec if client_options.open_timeout_sec
552+
request_options[:read_timeout] = client_options.read_timeout_sec if client_options.read_timeout_sec
553+
request_options[:write_timeout] = client_options.send_timeout_sec if client_options.send_timeout_sec
554+
options[:request] = request_options unless request_options.empty?
555+
options[:headers] = { 'User-Agent' => user_agent }
556+
557+
Faraday.new options do |faraday|
558+
faraday.response :logger, Google::Apis.logger if client_options.log_http_requests
559+
faraday.response :follow_redirects_google_apis_core, limit: 5
562560
end
563-
564-
if client_options.send_timeout_sec
565-
client.send_timeout = client_options.send_timeout_sec
566-
end
567-
568-
client.follow_redirect_count = 5
569-
client.default_header = { 'User-Agent' => user_agent }
570-
571-
client.debug_dev = logger if client_options.log_http_requests
572-
573-
# Make HttpClient use system default root CA path
574-
# https://github.com/nahi/httpclient/issues/445
575-
client.ssl_config.clear_cert_store
576-
client.ssl_config.cert_store.set_default_paths
577-
client
578561
end
579562

580563

google-apis-core/lib/google/apis/core/batch.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def decode_response_body(content_type, body)
8282
parts.each_index do |index|
8383
response = deserializer.to_http_response(parts[index])
8484
outer_header = response.shift
85-
call_id = header_to_id(outer_header['Content-ID'].first) || index
85+
call_id = header_to_id(Array(outer_header['Content-ID']).first) || index
8686
call, callback = @calls[call_id]
8787
begin
8888
result = call.process_response(*response) unless call.nil?
@@ -211,24 +211,24 @@ def to_http_response(call_response)
211211

212212
protected
213213

214-
# Auxiliary method to split the header from the body in an HTTP response.
214+
# Auxiliary method to split the headers from the body in an HTTP response.
215215
#
216216
# @param [String] response
217217
# the response to parse.
218-
# @return [Array<(HTTP::Message::Headers, String)>]
219-
# the header and the body, separately.
218+
# @return [Array<(Hash{String=>Array<String>}, String)>]
219+
# the headers and the body, separately.
220220
def split_header_and_body(response)
221-
header = HTTP::Message::Headers.new
221+
headers = {}
222222
payload = response.lstrip
223223
while payload
224224
line, payload = payload.split(/\n/, 2)
225225
line.sub!(/\s+\z/, '')
226226
break if line.empty?
227227
match = /\A([^:]+):\s*/.match(line)
228228
fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil?
229-
header[match[1]] = match.post_match
229+
(headers[match[1]] ||= []) << match.post_match
230230
end
231-
[header, payload]
231+
[headers, payload]
232232
end
233233
end
234234
end

google-apis-core/lib/google/apis/core/download.rb

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ module Core
2323
# Streaming/resumable media download support
2424
class DownloadCommand < ApiCommand
2525
RANGE_HEADER = 'Range'
26+
27+
# @deprecated No longer used
2628
OK_STATUS = [200, 201, 206]
2729

2830
# File or IO to write content to
@@ -61,8 +63,7 @@ def release!
6163
# of file content.
6264
#
6365
# @private
64-
# @param [HTTPClient] client
65-
# HTTP client
66+
# @param [Faraday::Connection] client Faraday connection
6667
# @yield [result, err] Result or error if block supplied
6768
# @return [Object]
6869
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@@ -78,39 +79,39 @@ def execute_once(client, &block)
7879
request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
7980
end
8081

81-
http_res = client.get(url.to_s,
82-
query: query,
83-
header: request_header,
84-
follow_redirect: true) do |res, chunk|
85-
status = res.http_header.status_code.to_i
86-
next unless OK_STATUS.include?(status)
82+
http_res = client.get(url.to_s, query, request_header) do |request|
83+
request.options.on_data = proc do |chunk, _size, res|
84+
status = res.status.to_i
85+
next if chunk.nil? || (status >= 300 && status < 400)
8786

88-
download_offset ||= (status == 206 ? @offset : 0)
89-
download_offset += chunk.bytesize
87+
# HTTP 206 is Partial Content
88+
download_offset ||= (status == 206 ? @offset : 0)
89+
download_offset += chunk.bytesize
9090

91-
if download_offset - chunk.bytesize == @offset
92-
next_chunk = chunk
93-
else
94-
# Oh no! Requested a chunk, but received the entire content
95-
chunk_index = @offset - (download_offset - chunk.bytesize)
96-
next_chunk = chunk.byteslice(chunk_index..-1)
97-
next if next_chunk.nil?
98-
end
91+
if download_offset - chunk.bytesize == @offset
92+
next_chunk = chunk
93+
else
94+
# Oh no! Requested a chunk, but received the entire content
95+
chunk_index = @offset - (download_offset - chunk.bytesize)
96+
next_chunk = chunk.byteslice(chunk_index..-1)
97+
next if next_chunk.nil?
98+
end
9999

100-
# logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
101-
@download_io.write(next_chunk)
100+
# logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
101+
@download_io.write(next_chunk)
102102

103-
@offset += next_chunk.bytesize
103+
@offset += next_chunk.bytesize
104+
end
104105
end
105106

106-
@download_io.flush if @download_io.respond_to?(:flush)
107+
@download_io.flush if @download_io.respond_to?(:flush)
107108

108109
if @close_io_on_finish
109110
result = nil
110111
else
111112
result = @download_io
112113
end
113-
check_status(http_res.status.to_i, http_res.header, http_res.body)
114+
check_status(http_res.status.to_i, http_res.headers, http_res.body)
114115
success(result, &block)
115116
rescue => e
116117
@download_io.flush if @download_io.respond_to?(:flush)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
require "faraday"
16+
require "faraday/follow_redirects"
17+
18+
module Google
19+
module Apis
20+
module Core
21+
# Customized version of the FollowRedirects middleware that does not
22+
# trigger on 308. HttpCommand wants to handle 308 itself for resumable
23+
# uploads.
24+
class FollowRedirectsMiddleware < Faraday::FollowRedirects::Middleware
25+
def follow_redirect?(env, response)
26+
super && response.status != 308
27+
end
28+
end
29+
30+
Faraday::Response.register_middleware(follow_redirects_google_apis_core: FollowRedirectsMiddleware)
31+
32+
# Customized subclass of Faraday::Response with additional capabilities
33+
# needed by older versions of some downstream dependencies.
34+
class Response < Faraday::Response
35+
# Compatibility alias.
36+
# Earlier versions based on the old `httpclient` gem used `HTTP::Message`,
37+
# which defined the `header` field that some clients, notably
38+
# google-cloud-storage, depend on.
39+
# Faraday's `headers` isn't an exact replacement because its values are
40+
# single strings whereas `HTTP::Message` values are arrays, but
41+
# google-cloud-storage already passes the result through `Array()` so this
42+
# should work sufficiently.
43+
alias header headers
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)