Railsのコントローラでの例外は直接ハンドリングしなくてもいい / 例外処理の方針

以下は静的なレスポンスを返す場合での方針です。動的なレスポンスならこの限りではありません。

基本方針

  1. 400番台以降のレスポンス(以下、例外レスポンス)を返したい場合でもコントローラであっても直接renderせず、例外をraiseする
  2. 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 の 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