Railsのbutton_to徹底解説：メソッドオーバーライドと隠しフィールドの仕組み

## 背景
業務の中でrailsのヘルパーメソッドである「button_to」について改めて調べ直す機会があったので、今回はその記録として記述をします。

## button_toとは？
button_toはRailsに組み込まれているヘルパーメソッドの一つです。button_toを使うと、指定したアクションへのフォームとボタンを簡単に生成できます。
```
= button_to "削除", "#", method: :delete
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670873/e1055eef-7744-becd-2f72-f67b45d8ef18.png)

HTMLではどうのように生成されているかというと、下記のようにformタグで囲われた一つのフォーム要素として生成されています。

```html
<form class="button_to" method="post" action="#">
    <input type="hidden" name="_method" value="delete" autocomplete="off">
    <button type="submit">削除</button><input type="hidden" name="authenticity_token"value="T9S0opNGpbhxWkdXu3mB9DQLA2nwGmORGRCHIrU_r2R6wp_qv0ypr7_0ogouh_efnlihvid3sI9Qnvi0UeiPcA" autocomplete="off">
</form>
```

よく見ると、謎のinput要素があります。
```html
<input type="hidden" name="_method" value="delete" autocomplete="off">」
```

誤って入ってしまったのかと思いrailsのコードを見てみましたが、「method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe」で確かにinputを作成していました。
```rb
 def button_to(name = nil, options = nil, html_options = nil, &block)
    html_options, options = options, name if block_given?
    html_options ||= {}
    html_options = html_options.stringify_keys

    url =
      case options
      when FalseClass then nil
      else url_for(options)
      end

    remote = html_options.delete("remote")
    params = html_options.delete("params")

    authenticity_token = html_options.delete("authenticity_token")

    method     = (html_options.delete("method").presence || method_for_options(options)).to_s
    method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe

    form_method  = method == "get" ? "get" : "post"
    form_options = html_options.delete("form") || {}
    form_options[:class] ||= html_options.delete("form_class") || "button_to"
    form_options[:method] = form_method
    form_options[:action] = url
    form_options[:'data-remote'] = true if remote

    request_token_tag = if form_method == "post"
      request_method = method.empty? ? "post" : method
      token_tag(authenticity_token, form_options: { action: url, method: request_method })
    else
      ""
    end

    html_options = convert_options_to_data_attributes(options, html_options)
    html_options["type"] = "submit"

    button = if block_given?
      content_tag("button", html_options, &block)
    elsif button_to_generates_button_tag
      content_tag("button", name || url, html_options, &block)
    else
      html_options["value"] = name || url
      tag("input", html_options)
    end

    inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
    if params
      to_form_params(params).each do |param|
        inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value],
                                   autocomplete: "off")
      end
    end
    html = content_tag("form", inner_tags, form_options)
    prevent_content_exfiltration(html)
  end

# actionview/lib/action_view/helpers/url_helper.rb
def method_tag(method)
  tag(:input, type: "hidden", name: "_method", value: method, autocomplete: "off")
end
```
参考先リンク：
https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/url_helper.rb#L295


本来であれば「form」と「button」があれば「必要ないのでは？？」と思いますが、これがないとPUT, PATCH, DELETEのようなHTTPメソッドを送信することができないのです。

もっと詳しく説明すると、HTMLフォームは標準ではGETまたはPOSTしかサポートしていません。PUT, PATCH, DELETEのようなHTTPメソッドを送信する方法が直接的にはありません。

しかし、Railsではこれを解決するために、「メソッドオーバーライド」という仕組みを使用しています。この仕組みでは、フォームのmethod="post"のままにしておき、「_method」という隠しフィールドを使って、サーバー側に「実際にはDELETEを使用する」と伝えることができます。

実際にRackのコードで_methodの値が有効なHTTPメソッド（例：PUT, DELETE）であれば、env[REQUEST_METHOD]を上書きしています。

該当コード：
```rb
def call(env)
  if allowed_methods.include?(env[REQUEST_METHOD])
    method = method_override(env)
    if HTTP_METHODS.include?(method)
      env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD]
      env[REQUEST_METHOD] = method
    end
  end

  @app.call(env)
end
```


コード全体：

```rb
# frozen_string_literal: true

require_relative 'constants'
require_relative 'request'
require_relative 'utils'

module Rack
  class MethodOverride
    HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]

    METHOD_OVERRIDE_PARAM_KEY = "_method"
    HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE"
    ALLOWED_METHODS = %w[POST]

    def initialize(app)
      @app = app
    end

    def call(env)
      if allowed_methods.include?(env[REQUEST_METHOD])
        method = method_override(env)
        if HTTP_METHODS.include?(method)
          env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD]
          env[REQUEST_METHOD] = method
        end
      end

      @app.call(env)
    end

    def method_override(env)
      req = Request.new(env)
      method = method_override_param(req) ||
        env[HTTP_METHOD_OVERRIDE_HEADER]
      begin
        method.to_s.upcase
      rescue ArgumentError
        env[RACK_ERRORS].puts "Invalid string for method"
      end
    end

    private

    def allowed_methods
      ALLOWED_METHODS
    end

    def method_override_param(req)
      req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data?
    rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError
      req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
    rescue EOFError
      req.get_header(RACK_ERRORS).puts "Bad request content body"
    end
  end
end
```
参考先リンク：
https://github.com/rack/rack/blob/main/lib/rack/method_override.rb

## まとめ
Railsのbutton_toは、フォームとボタンを簡単に生成し、HTML標準でサポートされないPUTやDELETEメソッドを送信するための「メソッドオーバーライド」が備わっています。これは隠しフィールド_methodを使用し、Rack::MethodOverrideがリクエストを適切に解釈する仕組みに基づいています。