LoginSignup
17
11

More than 5 years have passed since last update.

[Rails]リクエストのメソッドを見るならrequest#methodは避けるべきかも

Last updated at Posted at 2017-08-23

概要

みんなー、今日もがんばって仕事してるー!? まだまだ暑いのにえらい! (ゝω・)v
今日はちょっとrailsで引っかかったことについてのメモ記事だ。

要約してしまうとController内でふとHTTPリクエストのメソッドを参照したくなった時はrequest.methodよりもrequest.request_methodあるいはrequest.post?などを使うべきかも。何故ならDeleteメソッドなどを誤ってPostメソッドとしてしまうから。

コードと困った現象

例えばこんなアクションを書いてみよう。

cats_controller.rb
def cage
  result = {method: request.method,
    request_method: request.request_method,
    post?: request.post?
  }
  render json: result
end

内容は簡単で見ての通りHTTPリクエストについて調べようとActionDispatch::Requestオブジェクトであるrequestに#method、#request_method,#post?メソッドを使ったときの結果をjsonで返すだけの検証用コードだ。またこのアクションはget,post,deleteメソッドをどれも受ける。

まずは単純にGetメソッドを使ってみた場合の結果をみよう。

cage_get.json
{
  method: "GET",
  request_method: "GET",
  post?: false
}

うん、予想通りだね。これはいいんだ。

ではViewでlink_toを使ってdeleteメソッドを使ってみよう。

<%= link_to 'Delete Cage', cage_cats_path, method: :delete  %>
cage_delete.json
{
  method: "POST",
  request_method: "DELETE",
  post?: false
}

んん? なんで君はいきなりPOSTメソッドだと言い出したの?

request_method: リクエストメソッドを取得
method: HEAD以外は、「request.request_method」と同じ。headはgetを取得
post?: POSTかどうか

request - Railsドキュメント

みんなだいすきRailsドキュメントでは、同じメソッドを得るメソッド(ややこしいな)についてその違いを「HEADの場合だけ挙動が違うけど他はおんなじだよ〜」としている。でも、実際のところ2つのメソッドはもう少し違いがある。

解説と対処

これは実際の処理を追って見よう。

actionpack-4.2.4/lib/action_dispatch/http/request.rb
def method
  @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD'])
end

def request_method
  @request_method ||= check_method(env["REQUEST_METHOD"])
end

そう、2メソッドの違いはRackがメソッドを書き換えていた場合に生まれる。
methodメソッドは書き換えられていた場合に書き換えられる前のオリジナルのメソッドを返す。
request_methodメソッドは単に現在のメソッドを参照して返すだけだ。

Rackがメソッドを書き換えるだって? なんだそりゃ。
あ、Rackの説明は他の人の記事でも読んでね。 => Rackとは何か

簡単に言えばApacheやnginxといったWebサーバとRailsを繋いでくれる奴だよ。HTTPリクエストは最初からRailsアプリへ来るわけではなくてまずはWebサーバが受け取り、さらにrackを通してRailsアプリが処理する。

HTTPメソッドのオーバーライド

で、いったいどんな時にRackがメソッドの書き換えなんてしてくれるのか。そんなことをする必要ある?

rack-1.6.8/lib/rack/methodoverride.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

def method_override(env)
  req = Request.new(env)
  method = method_override_param(req) ||
    env[HTTP_METHOD_OVERRIDE_HEADER]
  method.to_s.upcase
end

軽く説明するとhttpのパラメータに"_method=(メソッド名)"っていう形式が含まれるときは、指定されてるメソッドとして扱うってワケ。なんでそんなことを?と思うかもね。それはHTMLのFORMタグがGET/POSTしかサポートしてなかったりするからだよ。そうした場合、どうするかっていうと、そうパラメータに_method=DELETEみたいに持たせることでアプリ側にDELETEメソッドとして読み替えてもらうんだよ。

そもそも、一番最初にlink_toを使ってdeleteメソッドのリクエストをしていたよね。でもよく考えるとaタグなんか踏んでもGETメソッドにしかならないはずだよね? 実際にレンダリングされるHTMLはこうだ。

<a rel="nofollow" data-method="delete" href="/cats/cage">Delete Cage</a>

見ての通り、data属性でdata-method="delete"としてる。だけど今のところはただのGETメソッドでリクエストされる。しかし、これをRailsのrails.jsがPOSTメソッドに変換してくれるんだ。
(より詳しい解説 => link_to に :method => :delete を指定した時の動作 )

処理的にはaタグが押された時、普通にGETメソッドを飛ばす代わりにPOSTメソッドのformタグ(_method="DELETE"パラメータを持つ)を作ってsubmitする。こうしてaタグでDELETEメソッドを使うことが出来てるってワケ。

現場からは以上です。╭( ・ㅂ・)و

17
11
0

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
17
11