URLビルダーがパラメータハッシュを破壊しないように修正
ActionDispatch::Http::URL.url_for が受け取った params ハッシュを破壊的に変更していた問題を、非破壊的な reject へ置き換えて解消しました。これにより memoized なリクエストパラメータが汚染されず、凍結ハッシュでも安全に利用できるようになります。
背景
url_for は内部で add_params を呼び出し、渡された params ハッシュから値が nil のエントリを除去してクエリ文字列を生成していました。元実装は params.reject! { |_, v| v.to_param.nil? } と 破壊的 な reject! を使用しており、呼び出し元のハッシュそのものが変更されました。
この破壊は二つの concrete な影響を引き起こします。まず request.query_parameters が memoized されているため、url_for が内部でハッシュを reject! すると以降のリクエスト処理で欠損したキーが見えなくなります。次に、呼び出し元が 凍結 (freeze) されたハッシュを渡した場合、reject! が例外 FrozenError を投げて URL が構築できませんでした。
この挙動は #23103 で修正された ActionDispatch::Routing::RouteSet::Generator の同様のバグと同系統であり、Rails の内部でリクエストスコープのハッシュを安全に扱う設計が求められていました。
技術的な変更
add_params メソッド内の破壊的呼び出しを非破壊的な reject に置き換えました。変更前後のコードは以下の通りです。
変更前:
def add_params(path, params)
params = { params: params } unless params.is_a?(Hash)
params.reject! { |_, v| v.to_param.nil? } # ← 破壊的変更
query = params.to_query
path << "?#{query}" unless query.empty?
end
変更後:
def add_params(path, params)
params = { params: params } unless params.is_a?(Hash)
params = params.reject { |_, v| v.to_param.nil? } # ← 非破壊的変更
query = params.to_query
path << "?#{query}" unless query.empty?
end
同時にテストスイートに二つの検証ケースを追加し、url_for が渡されたハッシュを変えないこと、凍結ハッシュでも例外が発生しないことを保証しました。
class RequestUrlFor < BaseRequestTest
# ... 既存テスト省略 ...
test "url_for does not mutate the passed params hash" do
params = { a: 1, b: nil }
assert_equal "http://www.example.com?a=1", url_for(params: params)
assert_equal({ a: 1, b: nil }, params)
end
test "url_for accepts a frozen params hash" do
assert_equal "/x?a=1", url_for(only_path: true, path: "/x", params: { a: 1, b: nil }.freeze)
end
end
結果として url_for の出力自体は変わらず、入力ハッシュはそのまま残ります。これにより memoized パラメータへの副作用が除去され、凍結ハッシュでも安全に利用可能です。
設計判断
この修正は 「入力オブジェクトは破壊しない」 という不変性の原則に沿った判断です。reject! が返す削除後のハッシュは使用されていなかったため、非破壊的 reject に置き換えるだけで機能的な差異は生じませんでした。
また、既存のパブリック API を変更せずに内部実装のみを調整した点が重要です。url_for の呼び出し側は従来通り params: オプションにハッシュを渡すだけで済み、追加のラッピングやオプションは不要です。これにより後方互換性を保ちつつ、内部状態の汚染リスクを排除しました。
まとめ
ActionDispatch::Http::URL.url_for がハッシュを破壊的に処理していたバグを、reject! → reject に置き換えるだけで修正し、テストで不変性と凍結ハッシュ対応を保証しました。結果としてリクエストパラメータの memoization が保護され、URL ビルダーの安全性が向上しています。