ネストしたルーティング制約の上書きバグを修正
複数の制約を入れ子にした場合に最内側の制約だけが有効になっていたバグが修正されました。この変更により、constraintsブロックとmountの:constraintsオプションを組み合わせた場合、すべての制約が正しく評価されます。
背景
constraintsブロックとmountの:constraintsオプションを同時に使用すると、外側の制約が無視されるという問題が#56528として報告されていました。この問題は、Deviseのauthenticateヘルパーのように制約を内部的に使用するライブラリと組み合わせた場合に特に顕在化します。
具体的には、以下のようなルーティング定義において、SomeIPConstraintとauthenticateの制約が完全に無視され、SomeReadOnlyConstraintのみが評価されていました:
constraints SomeIPConstraint do
authenticate :admin_user, ->(user) { user.can_access_sidekiq? } do
mount Sidekiq::Web => '/sidekiq', constraints: SomeReadOnlyConstraint
end
end
この挙動はドキュメントにも記載されておらず、警告も出ないため、開発者が気づかないまま認証・認可の制約が機能していない状態になるリスクがありました。
技術的な変更
修正の核心は、mapper.rb における制約の収集方式の変更です。変更は2か所に分かれており、それぞれが連携して動作します。
まず、blocksメソッドの実装が変更されました。このメソッドは単一の制約オブジェクトを受け取って配列に包んで返す役割を担っていましたが、複数の制約を受け取れるように拡張されました:
変更前:
def blocks(callable_constraint)
unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
end
[callable_constraint]
end
変更後:
def blocks(callable_constraints)
Array(callable_constraints).flatten.each do |c|
unless c.respond_to?(:call) || c.respond_to?(:matches?)
raise ArgumentError, "Invalid constraint: #{c.inspect} must respond to :call or :matches?"
end
end
end
次に、mountメソッド内で、スコープの制約とmountに渡された制約をマージする処理が追加されました:
# Merge scoped constraints and mount constraints
merged = []
merged.concat(Array(@scope[:blocks])) if @scope[:blocks]
merged.concat(Array(constraints)) if constraints
merged << @scope[:constraints] if @scope[:constraints].present?
constraints = merged if merged.any?
この処理により、スコープ由来の@scope[:blocks](ブロック形式の制約)、mountの:constraintsオプション、そしてスコープのハッシュ形式の制約(@scope[:constraints])が順番に収集され、すべてが評価対象となります。
テストでは、外側にfalseを返す制約、内側にtrueを返す制約を配置した場合に404が返ることを検証しています:
def test_lambda_mount_constraints_do_not_ignore_scoped_constraints
draw do
constraints ->(req) { false } do
mount lambda { |env| [200, {}, [env["REQUEST_METHOD"]]] },
at: "/test",
constraints: ->(req) { true }
end
end
get "/test"
assert_equal 404, status
end
外側のfalseを返す制約が正しく評価されることで404が返るようになり、修正前の誤動作(外側の制約が無視されて200が返る)を防いでいます。
設計判断
制約を「上書き」から「収集」に変えるアプローチが採用されました。
修正前はmountに:constraintsを渡すと@scope[:blocks]が置き換えられる動作になっていました。この修正では、ブロック制約・:constraintsオプション・スコープのハッシュ制約という3種類の制約ソースを明示的に列挙して結合しています。優先順位ではなく「すべてが満たされなければならない」というAND条件として組み合わせることで、どの制約も意図せずスキップされない設計になっています。
また、blocksメソッドの戻り値を廃止(配列を返さなくなった)したことで、バリデーション専用のメソッドとして責務が整理されている点も注目されます。
まとめ
この修正は、制約の「最内側が勝つ」という暗黙の上書きセマンティクスを「すべてを収集して評価する」に変えることで、ルーティング制約の直感的な合成を実現しています。認証・認可にルーティング制約を活用するアプリケーションでは、この変更によってネスト構造の設計がより安全かつ予測可能になります。