[rails/activeresource] 例外クラスに Net::HTTPRequest インスタンスを追加し、デバッグ性とリトライ可能性を向上
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 が実装されているため、既存のコードは引き続き動作しますが、将来的にはリクエストを含む新しいシグネチャへの移行が推奨されます。