Railsの ActionController::RoutingError は ApplicationController での rescue_from で捕まえられない

  • 8
    いいね
  • 0
    コメント

背景

  • Railsのエラーページを作っていて、ルーティングエラー時に404ページを表示したかった
  • 検索したり記憶を掘り返すと、rescue_from というので捕まえて404ページをrenderすればいいと思った
  • Rails Guides でも rescue_from が使われている。大丈夫だ
  • しかし捕まらない ActionController::RoutingError

コード

こんな感じでルーティングエラーを捕まえて404ページを表示できると思っていたのですが、捕まえることができませんでした。

application_controller.rb
  rescue_from ActionController::RoutingError, with: :render_404
  def render_404(exception = nil)
    render template: 'errors/error_404', status: 404, layout: 'application', content_type: 'text/html'
  end

原因

  • ActionController::RoutingError は ApplicationController の外で発生する
  • 検索して出て来る多くのページには以下のようなものが多かった
    • rescue_from ActionController::RoutingErrorを書き、config/routes.rbget '*path', to: 'errors#not_found'のようなルーティングを書く。
    • するとしっかりエラーページが表示される(しかしrescue_fromでActionController::RoutingErrorは捕まえられていない。)

つまり、ルーティング定義のワイルドカードでマッチさせてエラーページを出しているにもかかわらず、例外を捕まえた気になっていたのです。以前に書いたときはコピペで済ましたものの、今はrescueの意味くらいはわかるので違和感を持って調べたところ、そういうことだったというのがわかりました。

解決策

これは2通りの方法で解決できます。

  • 1. Rails標準のpublic/404.htmlなどを表示する(動的な表示はできない)
  • 2. Railsにはconfig.exceptions_appという設定があるので、そこにエラーを表示するためのRackアプリケーションを設定する

今回は2の手段をとりました。ErrorsControllerというApplicationControllerを継承したコントローラを作って、そのメソッドを呼ぶlambdaをconfig.exceptions_appに設定しました。

config/initializers/exceptions_app.rb
config.exceptions_app = ErrorsController.action(:render_error)

実際にエラーページを表示するところまでの詳細はRailsアプリの例外ハンドリングとエラーページの表示についてまとめてみた - Qiita に良い例が載っていました。

補足

上の、.actionとはどういうメソッドでしょうか。pryでおもむろにメソッドの定義を調べるとこんな感じです。

pry
[5] pry(ApplicationController):1> show-method ActionController::Metal.action

From: /Users/gaaamii/my_project/vendor/bundle/gems/actionpack-5.0.0.1/lib/action_controller/metal.rb @ line 240:
Owner: #<Class:ActionController::Metal>
Visibility: public
Number of lines: 15

def self.action(name)
  if middleware_stack.any?
    middleware_stack.build(name) do |env|
      req = ActionDispatch::Request.new(env)
      res = make_response! req
      new.dispatch(name, req, res)
    end
  else
    lambda { |env|
      req = ActionDispatch::Request.new(env)
      res = make_response! req
      new.dispatch(name, req, res)
    }
  end
end

Procオブジェクト(lambda)を返すようです。

[25] pry(ErrorsController):1> self.action(:show)
=> #<Proc:0x007ffa88ac6928@/Users/gaaamii/my_project/vendor/bundle/gems/actionpack-5.0.0.1/lib/action_contro

これをconfig.exceptions_app に設定するということは、例外を捕捉するコントローラを自分で定義したRackアプリケーションで差し替えるということになります。Procオブジェクトを設定しているのは、Rackがそういう(callに応答するオブジェクトをRackアプリケーションとする)仕様だからです。

なぜワイルドカードのルーティングではいけないのか

例外を捕捉する際、ApplicationControllerで捕捉できないのはルーティングエラーくらいだと考えられます。だとすれば、ルーティングエラーが発生しないように、意図したルーティング意外のものは全てマッチさせてエラーのコントローラにエラーページを出させてあげればいいのではないでしょうか。

実際これはよく使われていますが、何がいけないのでしょうか。あるエラーページ表示のQiita記事に対する @yuki24 さんのコメントを読み、理解することができました。

get '*path', to: ... や match '*something', to: ... はよく見かけますが、あまり良いプラクティスではないので、他の方法で実現した方がいいです。

  • match は既に非推奨
  • get なので、他の HTTP Method のリクエストに対応不能
  • ActionController::RoutingError は ApplicationController に到達する前に投げられる例外なので、rescue_from ActionController::RoutingError, ... は期待した通りに動かない(get '*path', to: ... によって、動いているように見えているだけ)
  • ApplicationController で rescue_from Exception, ... すると Airbrake や Bugsnag なんかが動かなくなる。

なるほどと思うのと同時に、まあ get '*path'でも多くの場合大丈夫そうだなという感想を持ちました。

まとめ

  • Rails的にはpublic/404.htmlのような静的ファイルでエラーを表示するのが真っ当なやりかた
  • とはいえエラーを自分で捕捉したいときのためにconfig.exceptions_appという設定が用意されてはいる
  • get '*path' でもそれほど困ることは無さそうだが、Railsエンジニアの間ではイマイチなやり方として認識されているような空気を感じた

参考