Railsのコントローラでの例外は直接ハンドリングしなくてもいい / 例外処理の方針
以下は静的なレスポンスを返す場合での方針です。動的なレスポンスならこの限りではありません。
基本方針
- 400番台以降のレスポンス(以下、例外レスポンス)を返したい場合でもコントローラであっても直接renderせず、例外をraiseする
- application.rb の
config.action_dispatch.rescue_responses
に例外とステータスコードのマッピングを書いていく
こんな感じ。
config.action_dispatch.rescue_responses = { "ActiveRecord::RecordNotFound" => :not_found }
デフォルトではこんな設定。 rails/exception_wrapper.rb at master · rails/rails · GitHub
rescue_fromは基本使わなくていい。
このやり方だとenvironmentごとに振る舞いがかわる。これはRails 6.0.3でのデフォルト設定での場合。
- development
- 例外がハンドリングされ、そのままRailsの詳細なエラーの画面として表示される
- test
- 詳細なデバッグ情報がHTTPレスポンスに出力されつつ、そのまま例外がスローされる(画面やレスポンスが描画されない)
- production
このままだとrequest spec等のテストのときに、期待するようなレスポンスコードの変換がされないので具合が悪い。それについては下記で補足する。
また、デフォルトでのexceptions_appの仕組みに則って描画されるので、ActionDispatch::PublicExceptions によって描画され、JSONの場合は rack/utils.rb に定義されているメッセージが返却される。ここは変更できないので、変更したい場合はexceptions_appを差し替えるか別の仕組みを利用することになる。
例外の定義
上記のやり方だとカスタム例外を定義していかないといけないが、処理を振り分けするだけのためにStandardErrorにラベル付けする程度の例外を定義しなければならず、中身が1行だけのファイルが増えて煩雑。 なので、こういう定義でラクをするのはどうだろうか。
app/models/exceptions.rb
module Exceptions class NotFound < StandardError; end class Unauth < StandardError; end end
なぜそうするか
開発中は、例外レスポンスは想定外な状況で起きることもあるので、発生した場所が分かった方がいい場合もある。
なぜそうなるのか
config.consider_all_requests_local
, config.action_dispatch.show_exceptions
の二つの設定が影響し、ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions の二つのRack Middlewareが操作されている。
ざっくりいうと、 config.consider_all_requests_local
がtrueならActionDispatch::DebugExceptionsがデバッグ画面を表示し、config.action_dispatch.show_exceptions
がtrueなら、ActionDispatch::ShowExceptionsが例外をハンドリングしてユーザー向けの描画をしている。
config.consider_all_requests_local: このフラグがtrueの場合、どのような種類のエラーが発生した場合にも詳細なデバッグ情報がHTTPレスポンスに出力され、アプリケーションの実行時コンテキストがRails::Infoコントローラによって/rails/info/propertiesに出力されます。このフラグはdevelopmentモードとtestモードではtrue、productionモードではfalseに設定されます。もっと細かく制御したい場合は、このフラグをfalseに設定してから、コントローラでlocal_request?メソッドを実装し、エラー時にデバッグ情報を出力したいリクエストをそこで指定してください。
ActionDispatch::ShowExceptions: アプリケーションから返されるすべての例外をrescueし、リクエストがローカルであるかconfig.consider_all_requests_localがtrueに設定されている場合に適切な例外ページを出力します。config.action_dispatch.show_exceptionsがfalseに設定されていると、常に例外が出力されます。
ref. Rails アプリケーションを設定する - Railsガイド
上記の環境ごとの設定と見比べると分かってくる。
- development
- 例外がハンドリングされず、そのままRailsの詳細なエラーの画面として表示される
config.consider_all_requests_local: true
config.action_dispatch.show_exceptions: true
- test
- 詳細なデバッグ情報がHTTPレスポンスに出力されつつ、そのまま例外がスローされる(画面やレスポンスが描画されない)
config.consider_all_requests_local: true
config.action_dispatch.show_exceptions: false
- production
refs.
- rails/railtie.rb at master · rails/rails · GitHub
- rails/debug_exceptions.rb at master · rails/rails · GitHub
- rails/show_exceptions.rb at master · rails/rails · GitHub
- Rails の exceptions_app によるエラーページの表示をテストする - Qiita
- Railsアプリの例外ハンドリングとエラーページの表示についてまとめてみた - Qiita
テストどうするか
Rails の exceptions_app によるエラーページの表示をテストする - Qiita に書いているとおり、要は、request specなどでは本番と同じ設定になっていればいい。ということなので、rspecではこんな感じで、実行時に設定を変更する。
shared_context 'Show Exceptions', show_exceptions: true do around(:each) do |example| show_detailed_exceptions = Rails.application.env_config['action_dispatch.show_detailed_exceptions'] show_exceptions = Rails.application.env_config['action_dispatch.show_exceptions'] Rails.application.env_config['action_dispatch.show_detailed_exceptions'] = false Rails.application.env_config['action_dispatch.show_exceptions'] = true example.run Rails.application.env_config['action_dispatch.show_detailed_exceptions'] = show_detailed_exceptions Rails.application.env_config['action_dispatch.show_exceptions'] = show_exceptions end end # `show_exceptions`メタを付けて実行する describe 'Get /hoge', show_exceptions: true do ... end