【コードリーディング】faraday-net_http
こんにちは、M-Yamashitaです。
今回の記事は、faraday-net_http gemのコードリーディングです。
以前Faraday gemのコードリーディングにて、HTTPライブラリのデフォルトのadapterとして、faraday-net_httpが指定されていました。
ここも気になっていたのでコードリーディングしました。
この記事で伝えたいこと
- faraday-net_http のコードリーディングによる内部実装の理解
faraday-net_http とは
GitHub の README から引用します。
This gem is a Faraday adapter for the Net::HTTP library. Faraday is an HTTP client library that provides a common interface over many adapters. Every adapter is defined into it's own gem. This gem defines the adapter for Net::HTTP the HTTP library that's included into the standard library of Ruby.
DeepLで翻訳すると以下のようになります。
このgemはNet::HTTPライブラリ用のFaradayアダプタです。FaradayはHTTPクライアントライブラリで、多くのアダプタに共通のインタフェースを提供します。すべてのアダプタはそれ自身のgemで定義されています。このgemは、Rubyの標準ライブラリに含まれているHTTPライブラリNet::HTTP用のアダプタを定義しています。
書いてあるとおりではあるのですが、まとめると、FaradayとRuby標準ライブラリに含まれているNet::HTTPライブラリの2つをつなぐものが、faraday-net_httpということです。
コードリーディング
環境
- Ruby 3.2.2
- faraday 2.7.11
- faraday-net_http 3.0.2
前提
Faradayを使ったサンプルコードは以下のようになります。
require 'faraday'
class FaradaySample
def request
# Create a Faraday connection object with retry middleware
conn = Faraday.new(url: 'https://example.com') do |faraday|
faraday.adapter Faraday.default_adapter
end
# Make a GET request to the /posts endpoint
response = conn.get('/')
# Print the response body
puts response.body
end
end
以前、このコードを使い、【コードリーディング】Faradayにて、コードリーディングを行いました。faraday-net_httpのコードリーディングは、このFaradayのコードリーディングの続きからとなります。
具体的には、
-
faraday-net_http の初期化
下記faradayのコードにて、hander.build
を呼び出した際に、faraday-net_httpを初期化しているdef to_app # last added handler is the deepest and thus closest to the inner app # adapter is always the last one @handlers.reverse.inject(@adapter.build) do |app, handler| handler.build(app) end end
def build(app = nil) klass.new(app, *@args, &@block) end
-
faraday-net_http の
call
メソッド呼び出しdef build_response(connection, request) app.call(build_env(connection, request)) end
の2つを見ます。
なお、faraday-net_httpのほとんどすべての処理は、lib/faraday/adapter/net_http.rbのみにまとめられています。アダプタというだけあって薄くまとまっています。
faraday-net_http の初期化
def initialize(app = nil, opts = {}, &block)
@ssl_cert_store = nil
super(app, opts, &block)
end
引数のapp
, opts
, block
は、debugでnil
となっていることがわかっています。またこの初期化では特段何もしておらず、親クラスを呼び出すのみです。net_http.rbの親クラスはFaraday::Adapter
となるので、そこを見ます。
def initialize(_app = nil, opts = {}, &block)
@app = ->(env) { env.response }
@connection_options = opts
@config_block = block
end
ここで@app
は引数のapp
を使わず、lamdaでenv.response
を返すことを指定しています。この初期化時点ではenv.response
にどんな値が入るのかは未定です。これが決まるのは後述のcall
メソッドのときです。あえてここで初期化したクラス等を設定していない理由としては、アダプタ側でenv.response
を自由に決められるようにという、faradayの思想なのかなと考えています。
faraday-net_httpの初期化については、短いですがここで終了です。
call
メソッド呼び出し
faraday-net_http のfaradayにて、以下コードのapp.call
により、faraday-net_httpのcall
メソッドが呼び出されます。
def build_response(connection, request)
app.call(build_env(connection, request))
end
faraday-net_httpのcall
メソッドは以下のとおりです。
def call(env)
super
connection(env) do |http|
perform_request(http, env)
rescue *NET_HTTP_EXCEPTIONS => e
raise Faraday::SSLError, e if defined?(OpenSSL) && e.is_a?(OpenSSL::SSL::SSLError)
raise Faraday::ConnectionFailed, e
end
@app.call env
rescue Timeout::Error, Errno::ETIMEDOUT => e
raise Faraday::TimeoutError, e
end
親クラスを呼び出したあと、コネクションを張ってリクエストを送るというシンプルな実装になっています。
では1つずつ見ていきます。まずは親クラスの呼び出しからです。
def call(env)
env.clear_body if env.needs_body?
env.response = Response.new
end
前述したように、このタイミングでenv.response
にResponse
クラスのインスタンスがセットされます。ここからわかるように、アダプタで特定のクラスのインスタンスを設定したいのであれば、superを使わず独自にenv.response
を定義すれば良さそうです。faraday-net_httpでは、faradayのデフォルトHTTPライブラリアダプタとして設定されているためか、親クラスの faraday のResponse
を使うことを基本としているようです。
親クラスの確認が終わったので、faraday-net_http 側のcall
メソッドに戻り、connection
メソッドを見ます。faraday-net_httpには実装されていないので、Faraday側を見ます。
def connection(env)
conn = build_connection(env)
return conn unless block_given?
yield conn
end
メソッドの中身としては、connectionを作り、ブロックがあればそれを実行というシンプルな実装です。
このconnectionメソッドがFaraday側に実装されている理由として考えられることは、接続としてのインターフェースはFaradayで提供するが、接続の仕方(build_connection
)はFaradayでは関与しないので子クラスに任せるという意図ですね。おそらくその意図のためなのか、Faraday側にはbuild_connection
メソッドはありません。
それではfaraday-net_http側のbuild_connection
メソッドを見てみます。
def build_connection(env)
net_http_connection(env).tap do |http|
http.use_ssl = env[:url].scheme == 'https' if http.respond_to?(:use_ssl=)
configure_ssl(http, env[:ssl])
configure_request(http, env[:request])
end
end
def net_http_connection(env)
proxy = env[:request][:proxy]
port = env[:url].port || (env[:url].scheme == 'https' ? 443 : 80)
if proxy
Net::HTTP.new(env[:url].hostname, port,
proxy[:uri].hostname, proxy[:uri].port,
proxy[:user], proxy[:password])
else
Net::HTTP.new(env[:url].hostname, port, nil)
end
end
ここではnet_http_connection
メソッドを呼び出し、Net::HTTPでの接続を作成しています。その後作成したhttpに対しconfigure_ssl
メソッド(sslの設定)やconfigure_request
メソッド(requestの設定)を行っています。これらのメソッドは設定のみであるため省略します。
では、net_http.rbのcall
メソッドに戻ります。
connection(env) do |http|
perform_request(http, env)
rescue *NET_HTTP_EXCEPTIONS => e
perform_request(http, env)
メソッドを見てみます。
def perform_request(http, env)
if env.stream_response?
http_response = env.stream_response do |&on_data|
request_with_wrapped_block(http, env, &on_data)
end
http_response.body = nil
else
http_response = request_with_wrapped_block(http, env)
end
env.response_body = encoded_body(http_response)
env.response.finish(env)
http_response
end
env.stream_response?
は、Faraday側でリクエストがストリームかどうかの判定をしています。今回はリクエストはストリームではないため、else句にあるrequest_with_wrapped_block
メソッドを見ます。
def request_with_wrapped_block(http, env)
# Must use Net::HTTP#start and pass it a block otherwise the server's
# TCP socket does not close correctly.
http.start do |opened_http|
opened_http.request create_request(env) do |response|
save_http_response(env, response)
if block_given?
response.read_body do |chunk|
yield(chunk)
end
end
end
end
end
ここではHTTPセッションを開始し、リクエストの送信とレスポンスの保存を行っています。
まずはcreate_request
メソッドを見てみます。
def create_request(env)
request = Net::HTTPGenericRequest.new \
env[:method].to_s.upcase, # request method
!!env[:body], # is there request body
env[:method] != :head, # is there response body
env[:url].request_uri, # request uri path
env[:request_headers] # request headers
if env[:body].respond_to?(:read)
request.body_stream = env[:body]
else
request.body = env[:body]
end
request
end
ここはNet::HTTPGenericRequest
のインスタンスを作成し、設定やbodyをセットするのみとなっています。このrequest
を使って、opened_http.request
でリクエストを実行しています。
受信したレスポンスはsave_http_response
メソッドで保存されているようです。
def save_http_response(env, http_response)
save_response(
env, http_response.code.to_i, nil, nil, http_response.message, finished: false
) do |response_headers|
http_response.each_header do |key, value|
response_headers[key] = value
end
end
end
save_response
の結果をもとに、レスポンスのヘッダーにセットしているようなので、save_response
メソッドを見ます。
def save_response(env, status, body, headers = nil, reason_phrase = nil, finished: true)
env.status = status
env.body = body
env.reason_phrase = reason_phrase&.to_s&.strip
env.response_headers = Utils::Headers.new.tap do |response_headers|
response_headers.update headers unless headers.nil?
yield(response_headers) if block_given?
end
env.response.finish(env) unless env.parallel? || !finished
env.response
end
save_response
メソッドはFaraday側に実装されています。
ここはfaraday-net_http側で独自のsave_response
メソッドを持つこともできたと思います。あえて親クラスのメソッドを呼び出している理由は、HTTPレスポンス結果をセットするクラスとしてFaraday側のResponse
を指定しているため、セット方法についてもFaraday側に合わせようという意図かなと思います。
それではコードリーディングに戻ります。save_response
メソッドではstatus
、body
をenv
に持たせenv.response.finish(env)
としています。env.response
は前述の通りResponse
クラスとなっているので、そのクラスのfinish
メソッドを見てみます。
def finish(env)
raise 'response already finished' if finished?
@env = env.is_a?(Env) ? env : Env.from(env)
@on_complete_callbacks.each { |callback| callback.call(@env) }
self
end
finish
メソッドでは、引数のenv
が持つデータを@env
に保存します。これにより、今までenv
に入っていたレスポンスの各種情報をResponse
クラスが扱えるようになります。
ではperform_request
メソッドに戻り、次の処理を見てみます。
def perform_request(http, env)
if env.stream_response?
http_response = env.stream_response do |&on_data|
request_with_wrapped_block(http, env, &on_data)
end
http_response.body = nil
else
http_response = request_with_wrapped_block(http, env)
end
env.response_body = encoded_body(http_response)
env.response.finish(env)
http_response
end
request_with_wrapped_block
でレスポンスの結果が返ってきたので、env.response_body
にレスポンスのボディをセットし、env.response
であるResponse
クラスをenv
で更新します。これでリクエストは終わりです。
最後に呼び出し元に返す処理が残っているので、call
メソッドに戻ります。
def call(env)
super
connection(env) do |http|
perform_request(http, env)
rescue *NET_HTTP_EXCEPTIONS => e
raise Faraday::SSLError, e if defined?(OpenSSL) && e.is_a?(OpenSSL::SSL::SSLError)
raise Faraday::ConnectionFailed, e
end
@app.call env
rescue Timeout::Error, Errno::ETIMEDOUT => e
raise Faraday::TimeoutError, e
end
@app.call env
では、@app
は@app = ->(env) { env.response }
となっていたので、env.response
であるResponse
クラスを呼び出し元に返します。
以上でfarday-net_httpのコードリーディングは終了です。
おわりに
今回はfaraday-net_httpのコードリーディングを行いました。
以前Faradayのコードリーディングをしていたこともあり、2つが実装としてどのようにつながるか、どんな思想を持っているかも合わせて確認できました。コードとドキュメントからどんな意図を持って作られたのか考えることは、それが合っていようと間違っていようと結構面白いと思います。
この記事が誰かのお役に立てれば幸いです。
Discussion