📚

【コードリーディング】Faraday

2023/11/28に公開

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

今回の記事は、Faraday gemのコードリーディングです。
こういうことができるgemだということは知っていましたが、内部実装まで踏み込んだことはありませんでした。
最近触る機会があったので、仕組みを知っておきたいと思い、コードリーディングをしました。

この記事で伝えたいこと

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

Faradayとは

https://github.com/lostisland/faraday

GitHubのREADMEから引用します。

Faraday is an HTTP client library abstraction layer that provides a common interface over many adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle.

DeepLで翻訳すると以下のようになります。

FaradayはHTTPクライアント・ライブラリの抽象化レイヤであり、多くのアダプタ(Net::HTTPなど)に共通のインターフェイスを提供し、リクエスト/レスポンス・サイクルを処理する際にRackミドルウェアの概念を取り入れている。

ここで大事なのは、HTTPクライアントライブラリであることとRackミドルウェアの概念を取り入れていることです。Faradayのドキュメントに、Fardayの仕組みをまとめられたページがありますので、参考にされてください。

https://lostisland.github.io/faraday/#/middleware/index

また、Rackについてはこちらを参考にされてください。

https://railsguides.jp/rails_on_rack.html

https://qiita.com/k0kubun/items/248395f68164b52aec4a

コードリーディング

環境

  • Ruby 3.2.2
  • Faraday 2.7.11

前提

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

コードリーディングでは上記コードのうち、以下2つを追っていきます。

  • Faradayの初期化(Faraday.new(url: 'https://example.com'))
  • Getリクエスト(conn.get('/'))

Faradayの初期化(Faraday.new

newメソッドは以下のとおりです。

faraday.rb
    def new(url = nil, options = {}, &block)
      options = Utils.deep_merge(default_connection_options, options)
      Faraday::Connection.new(url, options, &block)
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday.rb#L96

ここではoptionsを作成し、Faraday::Connectionのインスタンスを作成しています。
options作成でのdefault_connection_optionsメソッドを見てみます。

faraday.rb
    def default_connection_options
      @default_connection_options ||= ConnectionOptions.new
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday.rb#L127

初期化時点では@default_connection_optionsに何もセットしていないので、ConnectionOptions.newを実行します:

connection_options.rb
module Faraday
  ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url,
                                  :parallel_manager, :params, :headers,
                                  :builder_class) do
    options request: RequestOptions, ssl: SSLOptions

    memoized(:request) { self.class.options_for(:request).new }

    memoized(:ssl) { self.class.options_for(:ssl).new }

    memoized(:builder_class) { RackBuilder }

    def new_builder(block)
      builder_class.new(&block)
    end
  end
end

https://github.com/lostisland/faraday/blob/main/lib/faraday/options/connection_options.rb#L8

ConnectionOptionsにはOptionクラスのインスタンスをセットしています。OptionsクラスはStructの子クラスとなります。そのためConnectionOptionnsは、Optionsクラス(Struct)のインスタンスを保持しています。
https://github.com/lostisland/faraday/blob/main/lib/faraday/options.rb#L6

またConnectionOptions = Options.newの行を見てみると、ブロックを持っていることがわかります。ブロック内の処理をそれぞれ見ていきます。
まずは以下のoptionsの行をたどります。

module Faraday
  ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url,
                                  :parallel_manager, :params, :headers,
                                  :builder_class) do
    options request: RequestOptions, ssl: SSLOptions

optionsメソッドの定義は以下のとおりです。

options.rb
    def self.options(mapping)
      attribute_options.update(mapping)
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/options.rb#L156

options.rb
    def self.attribute_options
      @attribute_options ||= {}
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/options.rb#L161

ここではハッシュを持つ@attribute_optionsに、先ほどのrequest: RequestOptions, ssl: SSLOptionsをセットしています。

次はmemoizedの箇所を見ます。

memoized(:request) { self.class.options_for(:request).new }
memoized(:ssl) { self.class.options_for(:ssl).new }
memoized(:builder_class) { RackBuilder }

memoizedメソッドの定義は以下のとおりです。

options.rb
    def self.memoized(key, &block)
      unless block
        raise ArgumentError, '#memoized must be called with a block'
      end

      memoized_attributes[key.to_sym] = block
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        remove_method(key) if method_defined?(key, false)
        def #{key}() self[:#{key}]; end
      RUBY
    end

    def self.memoized_attributes
      @memoized_attributes ||= {}
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/options.rb#L170

memoizedメソッドでは、まずkeyblock@memoized_attributesにハッシュとして保持します。
次にclass_evalを使って動的にメソッドを定義します。今回はkeyrequestsslbuilder_classを渡しているので、以下のメソッドが動的に定義されます。

def request() self[:request]; end
def ssl() self[:ssl]; end
def builder_class() self[:builder_class]; end

ここまででdefault_connection_optionsの処理が終わりOptionsクラスのインスタンスを返すことがわかったので、options = Utils.deep_merge(default_connection_options, options)Utils.deep_mergeを追ってみます。(引数のoptionsは、今回何も指定していないのでnilとなります。)

utils.rb
    def deep_merge!(target, hash)
      hash.each do |key, value|
        target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))
                        deep_merge(target[key], value)
                      else
                        value
                      end
      end
      target
    end

    # Recursive hash merge
    def deep_merge(source, hash)
      deep_merge!(source.dup, hash)
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/utils.rb#L102

ここでは、targethashkeyvalueがあれば追加しています。valuetarget[key]のクラスによっては再帰的にdepp_merge!メソッドを呼び出して追加していくようです。
そのため、ユーザーがFaraday.newの引数optionsに何も指定しなければ、デフォルトのOptionsクラスのインスタンスを使用します。何か指定していればそのインスタンスに追加もしくは上書きしたインスタンスを使用するようです。

Faraday.new内のdeep_merge!メソッドを見終わったので、Farayday::Connection.newに移ります。

faraday.rb
    def new(url = nil, options = {}, &block)
      options = Utils.deep_merge(default_connection_options, options)
      Faraday::Connection.new(url, options, &block)
    end

Faraday::Connectionクラスの初期化メソッドは以下のとおりです。

connection.rb
    def initialize(url = nil, options = nil)
      options = ConnectionOptions.from(options)

      if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
        options = Utils.deep_merge(options, url)
        url     = options.url
      end

      @parallel_manager = nil
      @headers = Utils::Headers.new
      @params  = Utils::ParamsHash.new
      @options = options.request
      @ssl = options.ssl
      @default_parallel_manager = options.parallel_manager
      @manual_proxy = nil

      @builder = options.builder || begin
        # pass an empty block to Builder so it doesn't assume default middleware
        options.new_builder(block_given? ? proc { |b| } : nil)
      end

      self.url_prefix = url || 'http:/'

      @params.update(options.params)   if options.params
      @headers.update(options.headers) if options.headers

      initialize_proxy(url, options)

      yield(self) if block_given?

      @headers[:user_agent] ||= USER_AGENT
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/connection.rb#L63

行数が多いので、部分ごとに見ていきます。まずは、以下にあるoptionsurl変数へのセットを見ていきます。

      options = ConnectionOptions.from(options)

      if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
        options = Utils.deep_merge(options, url)
        url     = options.url
      end

引数で渡されたoptionsからConnectionOptionsを使用し、Optionクラスのインスタンスを作成します。また、Faraday.newの引数で指定されていたurlがハッシュならば、Utilsクラスを使ってurl keyのvalueを取り出します。今回はurl: 'https://example.com'と与えているので、'https://example.com'urlにセットされます。

次にインスタンス変数のセットを見てみます。

      @parallel_manager = nil
      @headers = Utils::Headers.new
      @params  = Utils::ParamsHash.new
      @options = options.request
      @ssl = options.ssl
      @default_parallel_manager = options.parallel_manager
      @manual_proxy = nil

      @builder = options.builder || begin
        # pass an empty block to Builder so it doesn't assume default middleware
        options.new_builder(block_given? ? proc { |b| } : nil)
      end

      self.url_prefix = url || 'http:/'

      @params.update(options.params)   if options.params
      @headers.update(options.headers) if options.headers

@manual_proxyへのセットまでは、基本的にデフォルト値がセットされます。また@builderについては、今回blockの指定があるので、上述のOptionsクラスで定義されていたnew_builerメソッドを、proc{ |b| }の引数付きで呼び出し、RackBuilderのインスタンスを作成しています。
このRackBuilderinitializeメソッドは以下のコードです。

rack_builer.rb
    def initialize(&block)
      @adapter = nil
      @handlers = []
      build(&block)
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/rack_builder.rb#L61

この初期化メソッドでのbuildメソッドを見てみます。

rack_builder.rb
    def build
      raise_if_locked
      block_given? ? yield(self) : request(:url_encoded)
      adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/rack_builder.rb#L73
raise_if_lockedメソッドについては、今回はnilとなります。次のblock_givenではブロックproc { |b| }は渡されていますが、空ブロックのため何も行いません。
次に、@adapterはまだセットされていないので、adapter(Faraday.default_adapter, **Faraday.default_adapter_options)を実行します。
adapterメソッド引数のFaraday.default_adapter, **Faraday.default_adapter_optionsの定義箇所は以下となります。

faraday.rb
  self.default_adapter = :net_http
  self.default_adapter_options = {}

https://github.com/lostisland/faraday/blob/main/lib/faraday.rb#L155

Faradayクラスの変数として定義されているので、クラスロード時に:net_http, {}がデフォルト値としてセットされています。
この引数を使ったadapterメソッドは以下のとおりです。

rack_builder.rb
    ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block)
      return @adapter if klass == NO_ARGUMENT || klass.nil?

      klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol)
      @adapter = self.class::Handler.new(klass, *args, &block)
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/rack_builder.rb#L110

klass:net_httpというシンボルなので、Faraday::Adapter.lookup_middleware(klass)を実行します。そのlookup_middlewareメソッドは、Faraday::MiddlewareRegistryモジュールに定義されています。

middleware_registry.rb
    def lookup_middleware(key)
      load_middleware(key) ||
        raise(Faraday::Error, "#{key.inspect} is not registered on #{self}")
    end

    def load_middleware(key)
      value = registered_middleware[key]
      case value
      when Module
        value
      when Symbol, String
        middleware_mutex do
          @registered_middleware[key] = const_get(value)
        end
      when Proc
        middleware_mutex do
          @registered_middleware[key] = value.call
        end
      end
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/middleware_registry.rb#L55

load_middlewareメソッドのregistered_middlewareをdebugで見てみると、以下のようになっていました。

(ruby) Faraday::Adapter.registered_middleware
{:test=>Faraday::Adapter::Test, :net_http=>Faraday::Adapter::NetHttp}

これらはFaradayの読み込み時にregistered_middlewareに登録されているようです。
そのため、lookup_middlewareメソッドでは、registered_middleware[key]:net_httpのキー指定によりFaraday::Adapter::NetHttpモジュールを取り出して、そのモジュールを返しています。

よってklassがそのモジュールとなるので、self.class::Handler.new(klass, *args, &block)を実行し、Handlerインスタンスを@adapterにセットします。

では、Faraday::Connectionクラスの初期化メソッドの確認に戻ります。
@param, @headersについては、options.params, options.headersがあればそれをセットします。今回はparamsheadersを指定していないのでスキップします。

Faraday::Connectionクラスの初期化メソッドの最後の部分です。

      initialize_proxy(url, options)

      yield(self) if block_given?

      @headers[:user_agent] ||= USER_AGENT

initialize_proxyメソッドは以下のとおりです。

connection.rb
    def initialize_proxy(url, options)
      @manual_proxy = !!options.proxy
      @proxy =
        if options.proxy
          ProxyOptions.from(options.proxy)
        else
          proxy_from_env(url)
        end
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/connection.rb#L96

options.proxyは今回指定していないので、@manual_proxyはfalseとなります。また、@proxyへのセットでは、proxy_from_envメソッドが呼ばれます。

connection.rb
    def proxy_from_env(url)
      return if Faraday.ignore_env_proxy

      uri = nil
      case url
      when String
        uri = Utils.URI(url)
        uri = if uri.host.nil?
                find_default_proxy
              else
                URI.parse("#{uri.scheme}://#{uri.host}").find_proxy
              end
      when URI
        uri = url.find_proxy
      when nil
        uri = find_default_proxy
      end
      ProxyOptions.from(uri) if uri
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/connection.rb#L513

Faraday.ignore_env_proxyはデフォルトでfalseのため、早期returnはスルーします。
url"https://example.com"なのでStringのケースに入り、Utils.URIを呼び出します。

utils.rb
    def URI(url) # rubocop:disable Naming/MethodName
      if url.respond_to?(:host)
        url
      elsif url.respond_to?(:to_str)
        default_uri_parser.call(url)
      else
        raise ArgumentError, 'bad argument (expected URI object or URI string)'
      end
    end

    def default_uri_parser
      @default_uri_parser ||= Kernel.method(:URI)
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/utils.rb#L71

urlStringのため、default_uri_parser.call(url)が実行されます。つまりKernel.method(:URI).call("https://example.com")となり、返り値はURI::HTTPSクラスのインスタンスです。

ではUtils.URIの呼び出し元のコードに戻ります。
uri.hostは存在するのでURI.parseのコードを実行しますが、proxyを設定していないのでfind_proxyメソッドの戻り値はnilとなります。そのため、ProxyOptions.from(uri)の実行はスキップされ、@proxynilとなります。

initialize_proxyメソッドの確認が終わったので以下の処理を見ていきます。

      yield(self) if block_given?

      @headers[:user_agent] ||= USER_AGENT

@headersについてはUSER_AGENTをセットしているのみです。その前のblock_givenについては、Faraday.newに以下blockが与えられているので、それを実行します。

    conn = Faraday.new(url: 'https://example.com') do |faraday|
      faraday.adapter Faraday.default_adapter
    end

このfaraday.adapterConnectionクラスにて定義されています。

connection.rb
    def_delegators :builder, :use, :request, :response, :adapter, :app

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/connection.rb#L120

adapterメソッドの処理は、buider、つまりRackBuilderインスタンスに移譲されています。
ここの Faraday.default_adapterをセットする処理は、上述した処理と同じであるため割愛します。

以上で、Faraday::Connection.new(url, options, &block)の確認が終わり、Faraday.newの処理はここで終わりです。

Faraday#getの呼び出し

Faradayの初期化が終了したので、以下getのコードリーディングをしていきます。

response = conn.get('/')

getメソッドは、Faraday::Connectionクラスでは動的に作成されます。

connection.rb
  class Connection
    # A Set of allowed HTTP verbs.
    METHODS = Set.new %i[get post put delete head patch options trace]

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/connection.rb#L17

connection.rb
    METHODS_WITH_QUERY.each do |method|
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{method}(url = nil, params = nil, headers = nil)
          run_request(:#{method}, url, nil, headers) do |request|
            request.params.update(params) if params
            yield request if block_given?
          end
        end
      RUBY
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/connection.rb#L197

そのため、動的に作成されたgetメソッドは以下のようになります。

def get(url = nil, params = nil, headers = nil)
  run_request(:get, url, nil, headers) do |request|
    request.params.update(params) if params
    yield request if block_given?
  end
end

run_requestメソッドが呼び出されるのでそれを見てみます。

connection.rb
    def run_request(method, url, body, headers)
      unless METHODS.include?(method)
        raise ArgumentError, "unknown http method: #{method}"
      end

      request = build_request(method) do |req|
        req.options.proxy = proxy_for_request(url)
        req.url(url)                if url
        req.headers.update(headers) if headers
        req.body = body             if body
        yield(req) if block_given?
      end

      builder.build_response(self, request)
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/connection.rb#L431

今回はgetメソッドを呼んでいるので、unlessには引っかかりません。
次にbuild_requestメソッドを呼び出します。

connection.rb
    def build_request(method)
      Request.create(method) do |req|
        req.params  = params.dup
        req.headers = headers.dup
        req.options = options.dup
        yield(req) if block_given?
      end
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/connection.rb#L453

Request.createとあるので、そこを見てみます。

request.rb
  Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do
    extend MiddlewareRegistry

    alias_method :member_get, :[]
    private :member_get
    alias_method :member_set, :[]=
    private :member_set

    # @param request_method [String]
    # @yield [request] for block customization, if block given
    # @yieldparam request [Request]
    # @return [Request]
    def self.create(request_method)
      new(request_method).tap do |request|
        yield(request) if block_given?
      end
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/request.rb#L27

Request.createメソッドではStructnewメソッドを呼び出しているのみとなります。
そのcreateメソッドではblockを呼び出しているので、build_requestメソッドのブロックを見てみます。

    def build_request(method)
      Request.create(method) do |req|
        req.params  = params.dup
        req.headers = headers.dup
        req.options = options.dup
        yield(req) if block_given?
      end
    end

ブロックではパラメータ、ヘッダーを指定しています。さらにbuild_requestメソッド呼び出し元でブロックがあれば、更にそれを呼び出します。それでは呼び出し元のrun_requestメソッドを見ます。

      request = build_request(method) do |req|
        req.options.proxy = proxy_for_request(url)
        req.url(url)                if url
        req.headers.update(headers) if headers
        req.body = body             if body
        yield(req) if block_given?
      end

ここではurl、ヘッダー、ボディを設定しています。更にrun_requestメソッドの呼び出し元にブロックがあればそれを呼び出します。
呼び出し元を見てみると、以下のようになっています。

def get(url = nil, params = nil, headers = nil)
  run_request(:get, url, nil, headers) do |request|
    request.params.update(params) if params
    yield request if block_given?
  end
end

paramsnilであることや、getメソッドの呼び出し元ではブロックを指定していないため、run_requestメソッドのブロックでは何も行いません。
ここまでで、yieldをたどる確認が終わったので、run_requestメソッドのbuilder.build_response(self, request)の確認に移ります。

connection.rb
    def run_request(method, url, body, headers)
      unless METHODS.include?(method)
        raise ArgumentError, "unknown http method: #{method}"
      end

      request = build_request(method) do |req|
        req.options.proxy = proxy_for_request(url)
        req.url(url)                if url
        req.headers.update(headers) if headers
        req.body = body             if body
        yield(req) if block_given?
      end

      builder.build_response(self, request)
    end

builderFaraday::Connectionクラスの初期化時に設定されたものとなります。今回はRackBuilderのインスタンスになるので、そのクラスのbuild_responseメソッドを見ます。

rack_builder.rb
    def build_response(connection, request)
      app.call(build_env(connection, request))
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/rack_builder.rb#L152

まずは引数にあるbuild_envメソッドを見ます。

rack_builder.rb
    def build_env(connection, request)
      exclusive_url = connection.build_exclusive_url(
        request.path, request.params,
        request.options.params_encoder
      )

      Env.new(request.http_method, request.body, exclusive_url,
              request.options, request.headers, connection.ssl,
              connection.parallel_manager)
    end

https://github.com/lostisland/faraday/blob/main/lib/faraday/rack_builder.rb#L201

build_excluesive_urlメソッドは以下のとおりです。

    def build_exclusive_url(url = nil, params = nil, params_encoder = nil)
      url = nil if url.respond_to?(:empty?) && url.empty?
      base = url_prefix.dup
      if url && !base.path.end_with?('/')
        base.path = "#{base.path}/" # ensure trailing slash
      end
      url = url.to_s.gsub(':', '%3A') if URI.parse(url.to_s).opaque
      uri = url ? base + url : base
      if params
        uri.query = params.to_query(params_encoder || options.params_encoder)
      end
      uri.query = nil if uri.query && uri.query.empty?
      uri
    end

https://github.com/lostisland/faraday/blob/3e27447a8016b6fc42829c1b413d82a6d0ea2d77/lib/faraday/connection.rb#L470

このメソッドでは、urlparams, params_encoderを組み合わせて、URIを作るメソッドとなります。

ではbuild_envメソッドに戻ります。次はEnv.newを見てみます。Envは、Structを親に持つOptionのインスタンスとなっています。今回のEnv.newではHTTPリクエストメソッドの:get、リクエストボディなどをセットします。

env.rb
  Env = Options.new(:method, :request_body, :url, :request,
                    :request_headers, :ssl, :parallel_manager, :params,
                    :response, :response_headers, :status,
                    :reason_phrase, :response_body) do

https://github.com/lostisland/faraday/blob/main/lib/faraday/options/env.rb#L57

それではbuild_envメソッドの呼び出し元である、app.callの処理に戻ります。

rack_builder.rb
    def build_response(connection, request)
      app.call(build_env(connection, request))
    end

appメソッドは以下のとおりです。

rack_builder.rb
    def app
      @app ||= begin
        lock!
        ensure_adapter!
        to_app
      end
    end

    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

https://github.com/lostisland/faraday/blob/main/lib/faraday/rack_builder.rb#L163

@handlersについては、今回何も追加のハンドラーを指定していないため、{}となっています。また@adapterFaraday::Adapter::NetHttpとなっています。そのため、Faraday::Adapter::NetHttpのインスタンスが@appに入ります。
よってapp.call(build_env(connection, request))Faraday::Adapter::NetHttpインスタンスのcallメソッド呼び出しとなります。

この先はFaradayのgemとは別のgemとなりますので、コードリーディングはここまでとなります。なお、callメソッドの返り値はFaraday::Responseとなります。

おわりに

今回はFaradayのコードリーディングを行いました。
追いかけるのが少し大変な部分もありましたが、比較的読みやすいコードだったと思います。
今後はFaradayが呼び出すことができるnet_httpや、stub、retryまでコードリーディングを広げたいですね。

この記事が誰かのお役に立てれば幸いです。

Discussion