ネストしたルーティング制約の上書きバグを修正

rails/rails

複数の制約を入れ子にした場合に最内側の制約だけが有効になっていたバグが修正されました。この変更により、constraintsブロックとmount:constraintsオプションを組み合わせた場合、すべての制約が正しく評価されます。

背景

constraintsブロックとmount:constraintsオプションを同時に使用すると、外側の制約が無視されるという問題が#56528として報告されていました。この問題は、Deviseのauthenticateヘルパーのように制約を内部的に使用するライブラリと組み合わせた場合に特に顕在化します。

具体的には、以下のようなルーティング定義において、SomeIPConstraintauthenticateの制約が完全に無視され、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メソッドの戻り値を廃止(配列を返さなくなった)したことで、バリデーション専用のメソッドとして責務が整理されている点も注目されます。

まとめ

この修正は、制約の「最内側が勝つ」という暗黙の上書きセマンティクスを「すべてを収集して評価する」に変えることで、ルーティング制約の直感的な合成を実現しています。認証・認可にルーティング制約を活用するアプリケーションでは、この変更によってネスト構造の設計がより安全かつ予測可能になります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
af0a266b

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術的な変更・設計判断(各論)、まとめ(結論)という「総論→各論→結論」の構成が明確で、非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:path/to/file```)やGitHubのIssueリンク記法([#56528](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Railsのルーティングの内部実装に関する内容であり、専門知識を持つエンジニアという対象読者に適切です。過度な説明が省かれており、簡潔です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが「総論→各論」の構成で書かれており、トピックセンテンスが段落の冒頭に配置されているため、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

`mapper.rb`と`routing_test.rb`のコード引用は、提供されたDiffの内容を正確に反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「constraintsブロック」「mount」「@scope[:blocks]」など、Railsのルーティングに関する技術用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

制約が「上書き」されていた問題が、複数の制約を「収集」する方式に変更されたという説明は、Diffのコード変更と完全に一致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescriptionやDiff、関連Issueから裏付けられており、ハルシネーション(創作)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#56718)やIssue番号(#56528)が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「ネストしたルーティング制約の上書きバグを修正」は、PRのタイトル「Fix innermost constraint reseting previous constraints」の内容を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PRで直接言及されていないバージョン情報やサポート状況などの外部知識の追加はなく、PRの内容に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「〜が修正されました」という過去形の表現は、完了した変更を報告する記事として適切であり、時間表現の歪曲はありません。