RailsのPathヘルパーを誤用してdeviseにハマった
はじめに
タイトル通りです。 deviseを使って認証機能を実装する際に、Pathヘルパーを誤用していたため、devise周りの機能がバグってしまいました。
この記事では、deviseでバグが発生した原因とその修正方法をまとめました。 また、何故deviseで期待した挙動にならないのかを理解するために、deviseのコードを追ってみました。
目次
バグの内容
サインアウトのリンクをクリック後に以下のバグが発生しました。
- リダイレクトしない。
- サインアウト後に通知メッセージが表示されない。
- 英語の場合、
en.devise.sessions.signed_out
キーの訳文が該当メッセージとなる。
- 英語の場合、
再現方法
今回発生した現象を再現するために、devise周りの設定方法について記載しました。
まず、バージョン情報は以下になります。
次にルーティングの設定です。config/routes.rb
にて以下のように設定をします。
Rails.application.routes.draw do devise_for :users # その他ルーティングの設定は省略。 end
サインアウトのprefix、メソッド、URIパターンは以下のようになります。
prefix | メソッド | URIパターン | アクション |
---|---|---|---|
destroy_user_session | DELETE | /users/sign_out(.:format) | devise/sessions#destroy |
Controllerの設定は不要です。 最後にViewの設定になります。任意のViewファイルで以下のようにPathヘルパーを使ってサインアウトのリンクを作ります。
<%= link_to 'サインアウト', destroy_user_session_path(current_user), method: :delete %>
current_user
はdeviseが用意したヘルパーメソッドです。サインイン中のユーザーのモデルオブジェクトを返します。
バグ発生の原因と対処方法
バグ発生の原因は、タイトルにある通り、Pathヘルパーの使い方に誤りがあるためです。
該当箇所は再現方法
で説明した「サインアウトのリンクを作成するコード」です。
Pathヘルパーのdestroy_user_session_path
のパラメータにcurrent_user
を指定したことが誤りになります。
destroy_user_session
のURIパターン/user/sign_out(.:format)
の:format
は、パラメータとなります。丸括弧()
は必須ではないパラメータを指します。パラメータ名がformat
なので、ここに入れるべき値は、html
やxml
などのMimeTypeのサブタイプが当てはまります。
しかし今回のケースでは、Pathヘルパーの引数にcurrent_userを指定してます。
実際のURLは/user/sign_out.2
のように、:format
箇所が
ログインユーザーのid
となります。フォーマットがdeviseの想定しない値であるため、今回のバグが発生したのです。
バグの対処方法は簡単です。
Pathヘルパーのパラメータに不適切な値を指定したので、これを修正することでバグは解消できます。URIパターンの丸括弧のパラメータは必須ではないため、下記のように、Pathヘルパーの引数を削除します。(引数なしでもリクエストでのフォーマットのMimeTypeのサブタイプはhtml
になります。)
<%= link_to 'サインアウト', destroy_user_session_path, method: :delete %>
サインアウト後にリダイレクトされない原因
根本的な原因は上述しましたが、deviseで期待した挙動にならない原因をコードリーディングで調査したいと思います。
サインアウト後のアクションは、devise/sessions#destroy
になるので、app/controllers/devise/sessions_controller.rb
のdestroy
メソッドを見ます。(deviseのファイルのパスは、bundle info devise
で確認が出来ます。)
最終行にrespond_to_on_destroy
メソッドを呼び出しているので、このメソッドがリダイレクト処理を実行しています。
def respond_to_on_destroy # We actually need to hardcode this as Rails default responder doesn't # support returning empty response on GET request respond_to do |format| format.all { head :no_content } format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name) } end end
respond_to
メソッドでは、クライアントの要求するフォーマットごとに処理を記述しています。 ブロック処理にはformat.all
(引数なし)とformat.any(*navigational_formats)
があります。format.all
とformat.any
は異なる機能のメソッドに見えますが、これらはエイリアスメソッドになります。
ですので、2つの処理が異なる点は引数の有無です。
引数の有無による挙動の違いについては、Rails APIのActionController::MimeResponds#respond_to
で以下のように説明があります。
引数有りの場合
respond_to
also allows you to specify a common block for different formats by usingany
:引数無しの場合
any
can also be used with no arguments, in which case it will be used for any format requested by the user:
つまり、引数有りの場合は、指定したフォーマットを対象に共通のブロック処理を実行します。また、引数がない場合は、任意のフォーマットを対象に共通のブロック処理を実行します。
respond_to_on_destroy
では、navigational_formatsに該当しないフォーマットの場合は、リクエスト時のページから遷移しない204 No Content
をレスポンスステータスとしています。
このことから、サインアウト後にリダイレクトしない原因は、destroy_user_session_path(current_user)
のフォーマットがnavigational_formats
に該当しないことだと予測できます。
では次に、navigational_formats
がどのフォーマットであるのか調べます。
app/controllers/devise_controller.rb
に該当するメソッドがあります。
def navigational_formats @navigational_formats ||= Devise.navigational_formats.select { |format| Mime::EXTENSION_LOOKUP[format.to_s] } end
配列のクラス変数で繰り返し処理を行っています。Mime::EXTENSION_LOOKUP
に含まれているフォーマットを抽出した要素がnavigational_formats
になるようです。
Mime::EXTENSION_LOOKUP
は、Railsで扱うフォーマットになります。
クラス変数のDevise.navigational_formats
は、lib/devise.rb
で定義しています。
# Which formats should be treated as navigational. mattr_accessor :navigational_formats @@navigational_formats = ["*/*", :html]
navigational_formats
は、Railsプロジェクトで設定可能な項目になります。
つまりlib/devise.rb
では、初期設定としてワイルドカードとhtml
がnavigational_formats
として定義されています。
以上のことから、調査内容をまとめます。
destroy_user_session_path(current_user)
のリクエストフォーマットはログイン中のユーザーid
である。- リクエストのフォーマットが
navigational_formats
に該当する場合は、リダイレクトする。 2.
以外の場合は、204 No Content
をレスポンスステータスを返す。つまり、画面遷移をしない。navigational_formats
は、今回のdeviseの設定では*/*
とhtml
- 今回のケースでリダイレクトしない原因は、リクエストフォーマットが
navigational_formats
に含まれず、2.
が実行されるため。
サインアウト後に通知メッセージが表示されない原因
こちらの原因もコードリーディングで調査したいと思います。
destroy
メソッドには、set_flash_message!
を呼び出しています。これが、通知メッセージを出力する処理となります。
# Sets flash message if is_flashing_format? equals true def set_flash_message!(key, kind, options = {}) if is_flashing_format? set_flash_message(key, kind, options) end end
デバッグすると、is_flashing_format?
の値が偽となり処理が終了します。
is_flashing_format?
を見てみます。
# Check if flash messages should be emitted. Default is to do it on # navigational formats def is_flashing_format? request.respond_to?(:flash) && is_navigational_format? end
最初の式の値は真となりましたが、2つ目の式の値は偽となりました。
ここでもnavigational_formatが出ました。is_navigational_format?
は、リクエストのフォーマットがDevise.navigatinal_formats
に含まれているかを判定します。
def is_navigational_format? Devise.navigational_formats.include?(request_format) end
Devise.navigational_formats
はリダイレクトの原因調査の際に確認しました。
つまり、通知メッセージが表示されない原因は、リダイレクトしない原因と同様にリクエストフォーマットがnavigational_formats
に含まれないためです。
まとめ
RailsのPathヘルパーを使用する際に、フォーマットのパラメータにdeviseが意図しない値を指定したことが原因でdevise周りの機能でバグが発生しました。 今回のバグの調査をしたことで、提供されるAPIメソッドの使い方と挙動を正確に理解することが重要であることを学びました。