背景
- Railsのエラーページを作っていて、ルーティングエラー時に404ページを表示したかった
- 検索したり記憶を掘り返すと、rescue_from というので捕まえて404ページをrenderすればいいと思った
- Rails Guides でも rescue_from が使われている。大丈夫だ
- しかし捕まらない ActionController::RoutingError
コード
こんな感じでルーティングエラーを捕まえて404ページを表示できると思っていたのですが、捕まえることができませんでした。
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.rb
にget '*path', to: 'errors#not_found'
のようなルーティングを書く。 - するとしっかりエラーページが表示される(しかし
rescue_from
でActionController::RoutingErrorは捕まえられていない。)
-
つまり、ルーティング定義のワイルドカードでマッチさせてエラーページを出しているにもかかわらず、例外を捕まえた気になっていたのです。以前に書いたときはコピペで済ましたものの、今はrescueの意味くらいはわかるので違和感を持って調べたところ、そういうことだったというのがわかりました。
解決策
これは2通りの方法で解決できます。
-
- Rails標準の
public/404.html
などを表示する(動的な表示はできない)
- Rails標準の
-
- Railsにはconfig.exceptions_appという設定があるので、そこにエラーを表示するためのRackアプリケーションを設定する
今回は2の手段をとりました。ErrorsControllerというApplicationControllerを継承したコントローラを作って、そのメソッドを呼ぶlambdaをconfig.exceptions_app
に設定しました。
config.exceptions_app = ErrorsController.action(:render_error)
実際にエラーページを表示するところまでの詳細はRailsアプリの例外ハンドリングとエラーページの表示についてまとめてみた - Qiita に良い例が載っていました。
補足
上の、.action
とはどういうメソッドでしょうか。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エンジニアの間ではイマイチなやり方として認識されているような空気を感じた
参考
- ruby on rails - rescue_from ActionController::RoutingError doesn't work - Stack Overflow
- RailsでAPIをつくるときのエラー処理 - Qiita
- Rails の rescue_from で拾えない例外を exceptions_app で処理する - Qiita
- Railsの404,500エラーページをカスタマイズ - Qiita
- Configuring Rails Applications — Ruby on Rails Guides
- Railsアプリの例外ハンドリングとエラーページの表示についてまとめてみた - Qiita