3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-25

背景

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

button_toとは?

button_toはRailsに組み込まれているヘルパーメソッドの一つです。button_toを使うと、指定したアクションへのフォームとボタンを簡単に生成できます。

= button_to "削除", "#", method: :delete

image.png

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

<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要素があります。

<input type="hidden" name="_method" value="delete" autocomplete="off">

誤って入ってしまったのかと思いrailsのコードを見てみましたが、「method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe」で確かにinputを作成していました。

 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]を上書きしています。

該当コード:

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

コード全体:

# 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がリクエストを適切に解釈する仕組みに基づいています。

3
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?