🐷

【コードリーディング】faraday-net_http

2024/01/27に公開

こんにちは、M-Yamashitaです。

今回の記事は、faraday-net_http gemのコードリーディングです。
以前Faraday gemのコードリーディングにて、HTTPライブラリのデフォルトのadapterとして、faraday-net_httpが指定されていました。
ここも気になっていたのでコードリーディングしました。

https://zenn.dev/m_yamashii/articles/code-reading-faraday/

この記事で伝えたいこと

  • faraday-net_http のコードリーディングによる内部実装の理解

faraday-net_http とは

https://github.com/lostisland/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 の初期化

net_http.rb
def initialize(app = nil, opts = {}, &block)
  @ssl_cert_store = nil
  super(app, opts, &block)
end

https://github.com/lostisland/faraday-net_http/blob/16ad984dfc0e2915addda17aa6879d8158ce32ad/lib/faraday/adapter/net_http.rb#L38

引数のapp, opts, blockは、debugでnilとなっていることがわかっています。またこの初期化では特段何もしておらず、親クラスを呼び出すのみです。net_http.rbの親クラスはFaraday::Adapterとなるので、そこを見ます。

adapter.rb
def initialize(_app = nil, opts = {}, &block)
  @app = ->(env) { env.response }
  @connection_options = opts
  @config_block = block
end

https://github.com/lostisland/faraday/blob/87e655f306454b49e459ac0a06d617cbad497fb4/lib/faraday/adapter.rb#L28

ここで@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メソッドは以下のとおりです。

net_http.rb
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

https://github.com/lostisland/faraday-net_http/blob/16ad984dfc0e2915addda17aa6879d8158ce32ad/lib/faraday/adapter/net_http.rb#L63

親クラスを呼び出したあと、コネクションを張ってリクエストを送るというシンプルな実装になっています。
では1つずつ見ていきます。まずは親クラスの呼び出しからです。

adapter.rb
def call(env)
  env.clear_body if env.needs_body?
  env.response = Response.new
end

https://github.com/lostisland/faraday/blob/87e655f306454b49e459ac0a06d617cbad497fb4/lib/faraday/adapter.rb#L55

前述したように、このタイミングでenv.responseResponseクラスのインスタンスがセットされます。ここからわかるように、アダプタで特定のクラスのインスタンスを設定したいのであれば、superを使わず独自にenv.responseを定義すれば良さそうです。faraday-net_httpでは、faradayのデフォルトHTTPライブラリアダプタとして設定されているためか、親クラスの faraday のResponseを使うことを基本としているようです。

親クラスの確認が終わったので、faraday-net_http 側のcallメソッドに戻り、connectionメソッドを見ます。faraday-net_httpには実装されていないので、Faraday側を見ます。

adapter.rb
  def connection(env)
    conn = build_connection(env)
    return conn unless block_given?

    yield conn
  end

https://github.com/lostisland/faraday/blob/87e655f306454b49e459ac0a06d617cbad497fb4/lib/faraday/adapter.rb#L41

メソッドの中身としては、connectionを作り、ブロックがあればそれを実行というシンプルな実装です。
このconnectionメソッドがFaraday側に実装されている理由として考えられることは、接続としてのインターフェースはFaradayで提供するが、接続の仕方(build_connection)はFaradayでは関与しないので子クラスに任せるという意図ですね。おそらくその意図のためなのか、Faraday側にはbuild_connectionメソッドはありません。

それではfaraday-net_http側のbuild_connectionメソッドを見てみます。

net_http.rb
  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

https://github.com/lostisland/faraday-net_http/blob/11953160f75dd488133a74857c2b07d41be8995d/lib/faraday/adapter/net_http.rb#L43

ここではnet_http_connectionメソッドを呼び出し、Net::HTTPでの接続を作成しています。その後作成したhttpに対しconfigure_sslメソッド(sslの設定)やconfigure_requestメソッド(requestの設定)を行っています。これらのメソッドは設定のみであるため省略します。

では、net_http.rbのcallメソッドに戻ります。

net_http.rb
  connection(env) do |http|
    perform_request(http, env)
  rescue *NET_HTTP_EXCEPTIONS => e

perform_request(http, env)メソッドを見てみます。

net_http.rb
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

https://github.com/lostisland/faraday-net_http/blob/11953160f75dd488133a74857c2b07d41be8995d/lib/faraday/adapter/net_http.rb#L95

env.stream_response?は、Faraday側でリクエストがストリームかどうかの判定をしています。今回はリクエストはストリームではないため、else句にあるrequest_with_wrapped_blockメソッドを見ます。

net_http.rb
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

https://github.com/lostisland/faraday-net_http/blob/11953160f75dd488133a74857c2b07d41be8995d/lib/faraday/adapter/net_http.rb#L109

ここではHTTPセッションを開始し、リクエストの送信とレスポンスの保存を行っています。
まずはcreate_requestメソッドを見てみます。

net_http.rb
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

https://github.com/lostisland/faraday-net_http/blob/11953160f75dd488133a74857c2b07d41be8995d/lib/faraday/adapter/net_http.rb#L79

ここはNet::HTTPGenericRequestのインスタンスを作成し、設定やbodyをセットするのみとなっています。このrequestを使って、opened_http.requestでリクエストを実行しています。
受信したレスポンスはsave_http_responseメソッドで保存されているようです。

net_http.rb
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

https://github.com/lostisland/faraday-net_http/blob/11953160f75dd488133a74857c2b07d41be8995d/lib/faraday/adapter/net_http.rb#L121

save_responseの結果をもとに、レスポンスのヘッダーにセットしているようなので、save_responseメソッドを見ます。

adapter.rb
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

https://github.com/lostisland/faraday/blob/87e655f306454b49e459ac0a06d617cbad497fb4/lib/faraday/adapter.rb#L62

save_responseメソッドはFaraday側に実装されています。
ここはfaraday-net_http側で独自のsave_responseメソッドを持つこともできたと思います。あえて親クラスのメソッドを呼び出している理由は、HTTPレスポンス結果をセットするクラスとしてFaraday側のResponseを指定しているため、セット方法についてもFaraday側に合わせようという意図かなと思います。
それではコードリーディングに戻ります。save_responseメソッドではstatusbodyenvに持たせenv.response.finish(env)としています。env.responseは前述の通りResponseクラスとなっているので、そのクラスのfinishメソッドを見てみます。

response.rb
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

https://github.com/lostisland/faraday/blob/87e655f306454b49e459ac0a06d617cbad497fb4/lib/faraday/response.rb#L49

finishメソッドでは、引数のenvが持つデータを@envに保存します。これにより、今までenvに入っていたレスポンスの各種情報をResponseクラスが扱えるようになります。

ではperform_requestメソッドに戻り、次の処理を見てみます。

net_http.rb
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メソッドに戻ります。

net_http.rb
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