Rails の Controller の params ってどうやって生成されているの?

Rails を使っている時 Controller で params を呼び出すと GET リクエストだったらクエリパラメータは取得できるし POST であれば Content-Type が multipart/form-data や application/json とかに関係なく値を取得できるようになっている。
便利!!!

Rails ガイドだとこのあたりに書いてある
railsguides.jp

ふとこの params ってどうやって生成されてるのかなーって気になったので Rails のコードを読みながら追ってみたので、そのメモをこの記事として残します。
※ もし間違っていたら教えてください 🙏

今回は Rails 6.0.3.4 を使用しています。

Controller の params は何?

Controller で params を使う時は RailsStrongParameters を使っていると思います。

params は下記の StrongParameters モジュール内の params メソッドから呼び出されているようです。
処理をみると request.parameters を引数にとって ActionController::Parametersインスタンスを生成しています。

def params
  @_params ||= Parameters.new(request.parameters)
end

https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_controller/metal/strong_parameters.rb#L1098-L1100

この ActionController::Parameters が Controller で params を使う時に呼び出す requirepermit などが定義されています。

どうやらこの request.parameters にほしい情報が入っているようです。

request.parameters

まず requestActionDispatch::Request です。
これは Controller からもアクセスできるので馴染みがあるかもですね。

request.parameters は ActionDispatch::Http::Parameters モジュール内に parameters メソッドとして定義されています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L49-L63

一部を抜粋したのが下記になります。

        params = begin
                   request_parameters.merge(query_parameters)
                 rescue EOFError
                   query_parameters.dup
                 end
        params.merge!(path_parameters)

request_parameters, query_parameters, path_parameters というそれっぽいものが使用されています。

それぞれ何なのかを見ていきます。

path_parameters

値としては {:controller=>"users", :action=>"create"} のような値が入っています。
リクエストされた URL に対してルーティングから選ばれた Controller とそのアクション名が入っているようです。

この path_parameters は下記で定義されていました。
処理としては get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {}) だけでした。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L83-L85

値の格納は下記のメソッドで行われているようです。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L66-L77

実際の値は ActionDispatch::Journey::Router 内の下記メソッドで格納しています。
この ActionDispatch::JourneyRails のルーティングを支えているライブラリのようです。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/journey/router.rb#L47-L47

query_parameters

次は query_parameters ですが、名前からもわかるように値としては例えば ?foo=bar のような URL でアクセスすると {"foo"=>"bar"} が入ってきます。

この query_parameters は下記で GET メソッドの alias として定義されています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/request.rb#L374-L384

      fetch_header("action_dispatch.request.query_parameters") do |k|
        rack_query_params = super || {}
        # Check for non UTF-8 parameter values, which would cause errors later
        Request::Utils.check_param_encoding(rack_query_params)
        set_header k, Request::Utils.normalize_encode_params(rack_query_params)
      end

fetch_header は rack gem に定義されているメソッドで下記に定義されています。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/request.rb#L68-L70

中身は Hash の fetch メソッドを読んでいるだけで、今回は block を定義しているので、キーが見つからなかった時にブロック内のコードが実行されます。
ここで重要なのでは rack_query_params = super || {} でここで super を呼び出すと rack に定義されている下記の GET メソッドが呼び出されます。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/request.rb#L426-L434

rack の GET メソッド内の query_hash = parse_query(query_string, '&;') で、query_string には "foo=bar" のような文字列が入っているのでこれがパースされて {"foo"=>"bar"} のような Hash になるようです。
そしてこの Hash が query_parameters の値として使用されます。

request_parameters

最後は request_parameters です。
path_parameters には Controller とアクション名が入っていて、query_parametersパラメータにはクエリパラメータの値が入っていました。
となると残りの request_parameters には POST 時の Body 情報が入ってそうです。

この request_parametes は下記に POST メソッドの alias として定義されています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/request.rb#L387-L397

一部を抜粋したのが下記のコードです。

        pr = parse_formatted_parameters(params_parsers) do |params|
          super || {}
        end

まず params_parsers は下記で定義されているメソッドで中身としては ActionDispatch::Request.parameter_parsers を呼び出します。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L130-L132

この ActionDispatch::Request.parameter_parsers は通常下記の DEFAULT_PARSERS が使われるようです。
デフォルトで使用される parsers は :json の場合しか定義されておらず、 :json の場合は raw_post という値を ActiveSupport::JSON.decode して Hash に変換しています。
raw_post は body の値を String として取り出したものを使うようです。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/request.rb#L319-L326

      DEFAULT_PARSERS = {
        Mime[:json].symbol => -> (raw_post) {
          data = ActiveSupport::JSON.decode(raw_post)
          data.is_a?(Hash) ? data : { _json: data }
        }
      }

https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L10-L15

parser を見たので次は parse_formatted_parameters の処理を見ていきます。
これは呼び出す際にブロックも指定しています。

        pr = parse_formatted_parameters(params_parsers) do |params|
          super || {}
        end

関数は下記で定義されています。
いくつか気になる処理がありますが、まず先ほどみた Hash の parsers から content_mime_type.symbol で値を取り出して、あれば後続の処理でその parser に raw_post (bodyをstringにしたもの) を渡して実行、なければ実行時に定義した block を実行しています。

        def parse_formatted_parameters(parsers)
          return yield if content_length.zero? || content_mime_type.nil?

          strategy = parsers.fetch(content_mime_type.symbol) { return yield }

          begin
            strategy.call(raw_post)
          rescue # JSON or Ruby code block errors.
            log_parse_error_once
            raise ParseError
          end
        end

https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L105-L116

まずは parsers の key として指定している content_mime_type.symbol が何のか見ていきます。
これは下記で定義されており、CONTENT_TYPEをみつつ値を返しています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L20-L29 マッピングは下記で定義されていて、例えば application/json なら :json, multipart/form-data なら :multipart_form です。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/mime_types.rb

先ほどのparse_formatted_parametersの処理に戻ります。
parsers には :json しか定義されていないので application/json の場合は先ほどDEFAULT_PARSERSで定義されていた :json の値を呼び出して ActiveSupport::JSON.decode した Hash を返してそうです。

そして multipart/form-data の場合は parser がないのでブロック内の処理が実行されます。
ブロック内は super || {} だけなのでこの super が Hash を返してそうです。

この super は rack の POST メソッドを呼び出します。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/request.rb#L440-L463

この処理ではいくつかの処理をしているようですが、今回は multipart/form-data の時の処理を追っていくので、下記の処理に注目します。

        elsif form_data? || parseable_data?
          unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
             ~~~
          end
          set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
          get_header RACK_REQUEST_FORM_HASH
        else

中でも set_header(RACK_REQUEST_FORM_HASH, parse_multipart) が重要で parse_multipartmultipart/form-data の時の body を下記のparserを使って Hash にして返しています。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/multipart/parser.rb

まとめ

最後にもう一度 params を呼び出した時の request.parameters の処理をみてみます。

        params = begin
                   request_parameters.merge(query_parameters)
                 rescue EOFError
                   query_parameters.dup
                 end
        params.merge!(path_parameters)

Bodyの情報が入った request_parameters とクエリパラメータの入った query_parameters がマージされてその後に Controller やアクション名の入った path_parameters がマージされます。

最後に

今回は Controller の params がどうやって生成されるかを見ていきました。