【コードリーディング】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 enddef 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の初期化については、短いですがここで終了です。
 faraday-net_http のcallメソッド呼び出し
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