Rate limiting が `by:` に渡すオブジェクトの `cache_key` を自動利用
Rate limiting の by: オプションが任意オブジェクトを受け取り、そのオブジェクトが cache_key を実装していれば自動的にキーとして利用できるようになりました。これにより、IP アドレスに依存しないユーザー単位のレート制御がシンプルに記述可能になります。
背景
デフォルトのレート制限は リクエスト元の IP アドレス でスコープされていますが、同一ネットワーク内で複数の認証済みユーザーが存在するケースでは不適切です。ユーザーごとに個別の制限を設けるには従来、by: に current_user.cache_key のように手動でキーを生成する必要があり、コードベースに散在しやすくなります。本 PR はその手間を削減し、by: にユーザーオブジェクト自体を渡すだけで cache_key が利用されるデフォルト動作を提供します。
技術的な変更
rate_limit メソッド内部で by 引数を評価した後、by.respond_to?(:cache_key) で判定し、存在すればその結果をキーとして採用するロジックが追加されました。変更点は次の通りです。
変更前(抜粋):
def rate_limiting(to:, within:, by:, with:, store:, name:, scope:)
by = by.is_a?(Symbol) ? send(by) : instance_exec(&by)
to = to.is_a?(Symbol) ? send(to) : (to.respond_to?(:call) ? instance_exec(&to) : to)
within = within.is_a?(Symbol) ? send(within) : (within.respond_to?(:call) ? instance_exec(&within) : within)
# ...
end
変更後(抜粋):
def rate_limiting(to:, within:, by:, with:, store:, name:, scope:)
by = by.is_a?(Symbol) ? send(by) : instance_exec(&by)
+ by = by.cache_key if by.respond_to?(:cache_key)
to = to.is_a?(Symbol) ? send(to) : (to.respond_to?(:call) ? instance_exec(&to) : to)
within = within.is_a?(Symbol) ? send(within) : (within.respond_to?(:call) ? instance_exec(&within) : within)
# ...
end
このシンプルな分岐追加により、既存の by: 文字列やシンボルの使用法はそのまま動作し、cache_key を持つオブジェクトを渡すだけでキーが生成されます。公式ドキュメントも CHANGELOG に記載され、使用例として次のように記述できます。
class CommentsController < ApplicationController
# キャッシュキーは "rate-limit:comments:user/1" になる
rate_limit to: 2, within: 2.seconds, by: -> { current_user }
private
def current_user
User.find(1)
end
end
テストコードも ModelWithKey という Struct に cache_key を実装し、同一キーに対するリクエストが制限され、別キーでは独立してレート制御が行われることを検証しています。
設計判断
本変更は 後方互換性を最小限のコード増加で確保 する設計です。by: に文字列やシンボルを渡す既存の呼び出しは影響を受けず、cache_key を実装したオブジェクトだけが新たな挙動を得ます。PR 内の議論では別キー by_cache_key: を導入する案も検討されましたが、設定キーの増加は Rails の DSL のシンプルさを損ねるとの判断で、既存キーの拡張路線が採られました。結果として、レート制限のスコープをオブジェクト単位に拡張できるが、既存コードベースへの侵入は極めて低く抑えられました。
まとめ
by: にオブジェクトを渡すだけでその cache_key がレート制限キーとして使用されるようになり、IP 依存からユーザー単位へのスコープ変更が簡潔に実装可能になりました。この変更は既存の DSL を壊さず、テストで挙動を裏付けているため、すぐにプロジェクトへ導入できる実用的な拡張と言えます。