認証機能にDeviseを利用しているシステムで「Routing Error」によりサインアウトできない場合の対処

はじめに

最初に述べておくと、実はDeviseは何も関係ない。jqueryjquery_ujsのバージョンには気をつけろというお話。

環境

Gemfileの中身は以下の通り。

gem 'rails', '3.2.11'
gem 'devise', '3.2.4'
gem 'jquery-rails', '3.1.0'

問題

ある画面でリンクを用意し、そのリンクを押下してサインアウトしようとすると、、、

<a href="/users/sign_out" data-method="delete" rel="nofollow">サインアウト</a>

以下の「Routing Error」がでてしまう。

No route matches [GET] "/users/sign_out"

ルーティングを見てみると、

% bundle exec rake routes | grep sign_out
        destroy_user_session DELETE /users/sign_out(.:format)                                 devise/sessions#destroy

サインアウトはDELETEメソッドで呼び出す必要がある事が分かる。 エラーの通りGETで呼び出してしまっている事が問題という事が分かった。

またもう一つの事実として 問題はjquery1.10.2をロードしている画面のみで発生しており、 少し古いjquery1.6.2をロードしている画面では発生しない(サインアウトできる)ことが分かった。

TwitterのBootstapを利用する為に新しいjqueryを導入する必要があった訳だが、思わぬところでつまずいてしまった。

調査

Railsでは現在GET/POSTしかないHTTPでCRUD操作を実現する為に、formのinputタグのnameに「_method」を指定する。 CRUDのD(DELETE)を行う場合は<input name="_method" type="hidden" value="delete" />と指定する。

http://guides.rubyonrails.org/form_helpers.html

今回の問題が発生したのはaタグで生成したリンクだが、実はこのリンクは押下時にformタグに置き換えられている。 これを実現しているのは、jquery_ujs.jsというスクリプトでGemの「jquery-rails」によってインストールされる。 linkClickSelectorメソッド内で呼び出されるhandleMethodメソッド内でリンクがformタグに置き換わっている事が分かる。 「data-method」で「delete」と指定していれば<input name="_method" value="delete" type="hidden" />と置き換わる。

…

$(rails.linkClickSelector).live('click.rails', function(e) {
    var link = $(this);
    if (!rails.allowAction(link)) return rails.stopEverything(e);

    if (link.data('remote') !== undefined) {
      rails.handleRemote(link);
      return false;
    } else if (link.data('method')) {
      rails.handleMethod(link);                                                                                                                                 
      return false;
    }
  });

…

handleMethod: function(link) {
      var href = link.attr('href'),
        method = link.data('method'),
        csrf_token = $('meta[name=csrf-token]').attr('content'),
        csrf_param = $('meta[name=csrf-param]').attr('content'),
        form = $('<form method="post" action="' + href + '"></form>'),                                                                                          
        metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';

      if (csrf_param !== undefined && csrf_token !== undefined) {
        metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
      }

      form.hide().append(metadata_input).appendTo('body');
      form.submit();
    }

…

原因

ここまで調査して、ようやくブラウザのデバッグ用コンソールに以下のメッセージが出力されている事に気付く。

[Error] TypeError: undefined is not a function (evaluating '$(rails.linkClickSelector).live')
    (anonymous 関数) (jquery_ujs.js, line 266)

最初はlinkClickSelectorがないと言われているのかと思ったが、「liveメソッドがない」という事だった。 調べていくと、jquery1.9以降にliveメソッドが存在しないという事が分かった。

stackoverflow.com

後一歩だ。

jquery_ujs.jsが古いという事は、jquery-railsのバージョンが古いはず!と思ったのに、GemのバージョンはRails3系のもので問題なさそう。

qiita.com

こちらの記事を参考にしてみると、以下のメッセージがでた。

% bundle exec rails g jquery:install
  deprecated  You are using Rails 3.1 with the asset pipeline enabled, so this generator is not needed.
              The necessary files are already in your asset pipeline.
              Just add `//= require jquery` and `//= require jquery_ujs` to your app/assets/javascripts/application.js
              If you upgraded your app from Rails 3.0 and still have jquery.js, rails.js, or jquery_ujs.js in your javascripts, be sure to remove them.
              If you do not want the asset pipeline enabled, you may turn it off in application.rb and re-run this generator.

まとめると、以下を行えば良さそうだ。

  • Rails3.1でasset pipelineがenableの場合、このコマンドでjsをインストールする必要がある。
  • app/assets/javascripts/application.jsに「//= require jquery」および「//= require jquery_ujs」を追記する。
  • Rails3.0からアップグレードした場合、jquery.js/rails.js/jquery_ujs.jsを削除しなければならない。

対処

今回の問題は asset pipelineを有効にしていても、app/assets/javascripts配下に古いバージョンのjsが存在するとそちらを優先する為に発生していたようだ。 古いバージョンのjsを削除し、以下のように設定すると問題なく動作した。

% cat app/assets/javascripts/application.js
// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults
//
//= require 'jquery_ujs'
% cat app/views/layouts/hoge.html.erb
<head>
…
    <%= stylesheet_link_tag 'bootstrap_flatly/bootstrap.css' %>
    <%= stylesheet_link_tag 'bootstrap_flatly/usebootstrap.css' %>
    <%= stylesheet_link_tag 'bootstrap_flatly/dashboard.css' %>
    <%= stylesheet_link_tag 'bootstrap_flatly/sticky-footer-navbar.css' %>
    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <%= javascript_include_tag 'bootstrap_flatly/html5shiv.js' %>
      <%= javascript_include_tag 'bootstrap_flatly/respond.min.js' %>
    <![endif]-->
    <%= javascript_include_tag 'https://code.jquery.com/jquery-1.10.2.min.js' %>★←jqueryはapplication.js内ではロードさせない
    <%= javascript_include_tag 'bootstrap_flatly/bootstrap.min.js' %>
    <%= javascript_include_tag 'bootstrap_flatly/usebootstrap.js' %>
    <%= javascript_include_tag 'jquery-ui-1.8.16.custom.min.js' %>
    <%= javascript_include_tag 'jquery.inputtips-1.0.0-min.js' %>
    <%= javascript_include_tag :application %>★←application.jsのロード
…
</head>

上記は参考までにBootstrapのflatlyというテーマとダッシュボード用のテーマを導入した際にロードしたcssとjs。 Bootstrapのテーマが古いjqueryだと動作しないため、application.jsでjquery.jsをロードしないように注意が必要。