DiffDaily

Deep & Concise - OSS変更の定点観測

[rails/activeresource] 例外クラスに Net::HTTPRequest インスタンスを追加し、デバッグ性とリトライ可能性を向上

rails/activeresource

Context

ActiveResourceで発生する例外(タイムアウト、SSL エラー、接続拒否など)に対して、どのHTTPリクエストが失敗したのかを特定する情報が不足していました。例外をrescueする側では、デバッグ、ログ記録、リクエストのリトライなどを実装する際に、元のリクエスト情報(HTTPメソッド、URI、ヘッダーなど)にアクセスできることが重要です。

2ea5401のコミットで、接続ライフサイクル全体を通じて Net::HTTPRequest インスタンスが利用可能になったことを受け、この変更では例外オブジェクトにリクエスト情報を含めるようにしました。

Technical Detail

例外クラスのコンストラクタ変更

ConnectionError およびそのサブクラスのコンストラクタが、第一引数として Net::HTTPRequest インスタンスを受け取るように変更されました。

変更後のシグネチャ:

class ConnectionError < StandardError
  attr_reader :request, :response

  def initialize(request, response = nil, message = nil)
    @request  = request
    @response = response
    @message  = message
  end

  def to_s
    return @message if @message

    message = +"Failed."
    message << "  Request = #{request.method} #{request.uri}." if request.respond_to?(:method) && request.respond_to?(:uri)
    message << "  Response code = #{response.code}." if response.respond_to?(:code)
    message << "  Response message = #{response.message}." if response.respond_to?(:message)
    message
  end
end

エラーメッセージには、リクエストのHTTPメソッドとURIが自動的に含まれるようになり、デバッグが容易になります。

後方互換性のための deprecation 処理

例外クラスのコンストラクタは公開APIの一部であるため、引数の順序を変更することは破壊的変更となります。そのため、古いシグネチャで呼び出された場合に deprecation warning を出力する処理が追加されました。

def initialize(request, response = nil, message = nil)
  if request.is_a?(Net::HTTPResponse) && (response.is_a?(String) || response.nil?)
    ActiveResource.deprecator.warn(<<~WARN)
      ConnectionError subclasses must be constructed with a request. Call super with a Net::HTTPRequest instance as the first argument.
    WARN

    message = response
    response, request = request, nil
  end
  # ...
end

これにより、サブクラスを定義しているユーザーコードに対して、段階的な移行パスを提供します。

Connection クラスでの例外生成の変更

HTTPリクエストを実行する Connection#request メソッドおよび Connection#handle_response メソッドで、例外を生成する際に Net::HTTPRequest インスタンスを渡すように変更されました。

変更前:

rescue Timeout::Error => e
  raise TimeoutError.new(e.message)
rescue OpenSSL::SSL::SSLError => e
  raise SSLError.new(e.message)

変更後:

rescue Timeout::Error => e
  raise TimeoutError.new(request, e.message)
rescue OpenSSL::SSL::SSLError => e
  raise SSLError.new(request, e.message)

同様に、HTTPレスポンスコードに基づいて例外を発生させる handle_response メソッドも更新されました。

変更前:

def handle_response(response)
  case response.code.to_i
  when 401
    raise(UnauthorizedAccess.new(response))
  when 404
    raise(ResourceNotFound.new(response))
  # ...
end

変更後:

def handle_response(request, response)
  case response.code.to_i
  when 401
    raise(UnauthorizedAccess.new(request, response))
  when 404
    raise(ResourceNotFound.new(request, response))
  # ...
end

リクエスト情報へのアクセス例

例外をrescueする側では、以下のようにリクエスト情報を取得できます。

begin
  Person.find(1)
rescue ActiveResource::ResourceNotFound => e
  Rails.logger.error "Failed to fetch resource: #{e.request.method} #{e.request.uri}"
  # リトライ処理など
  retry_request(e.request)
end

テストの更新

例外に request 属性が正しく設定されていることを確認するため、テストケースが追加されました。

def test_timeout
  @http = mock("new Net::HTTP")
  @conn.expects(:http).returns(@http)
  @http.expects(:request).raises(Timeout::Error, "execution expired")
  error = assert_raise(ActiveResource::TimeoutError) { @conn.get("/people_timeout.json") }
  assert_kind_of Net::HTTPRequest, error.request
end

まとめ

この変更により、ActiveResourceの例外処理において以下が可能になりました。

  • デバッグの容易化: エラーメッセージに自動的にリクエスト情報が含まれる
  • 詳細なログ記録: リクエストのメソッド、URI、ヘッダーなどを記録できる
  • リトライ機能の実装: 失敗したリクエストを再試行する際に元のリクエスト情報を利用できる

後方互換性を保ちながら段階的に移行できるよう、deprecation warning が実装されているため、既存のコードは引き続き動作しますが、将来的にはリクエストを含む新しいシグネチャへの移行が推奨されます。