Railsで動的にエラーページを表示するGemを公開しました

Railsで404や500といったエラーページを動的に表示するrexceptionというGemを公開しました。

インストール方法

Gemfileに以下を書いてbundleするだけで使えます。

gem 'rexception'

使い方

最もシンプルな使い方は、app/views/errors/application.html.erbにビューを配置するだけです。 これで、(捕捉可能な)すべての種類のエラー発生時に、対応するステータスコードとともにビューをレンダリングします。

not_found.html.erbinternal_server_error.html.erbなど、ステータスに対応するビューを配置した場合、そのビューがapplication.html.erbよりも優先して表示されます(ファイル名はこの辺りの実装に準じています)。

また、レイアウトやエラーファイル配置ディレクトリ、独自のエラーハンドラの追加が可能です。 config/initializers/rexception.rbを作成し、以下を参考に設定します。

Rexception.setup do |config|
  # Specify the layout file to use for rendering error page.
  # config.layout = 'application'

  # Specify the directory where you place error pages.
  # config.errors_dir = 'errors'

  # Define which of statuses return against custom exceptions.
  # config.rescue_responses = {
  #   'CustomException' => :not_found
  # }
end

development環境でエラーページを確認するには、config/environments/development.rbで以下の設定を行ないます。

config.consider_all_requests_local = false

原理

Rexceptionの考え方としては、Rails標準のPublicExceptionsを独自のコントローラに差し替えることで実現しています(あわせてActionDispatch::ShowExceptionsのコードが参考になります)。

処理の大まかな流れは次のとおりです:

  1. env['action_dispatch.exception']にエラー内容が格納される
  2. これを元にActionDispatch::ExceptionWrapper.rescue_responsesからステータス(:not_foundなど)を取得する
  3. これと同名のメソッド(def not_foundなど)を事前に動的生成しておき、エラーの際はこのメソッドを返す
    • このメソッドは、対応するビュー(not_found.html.erbなど)があればそれを、なければapplication.html.erbを表示する

なお、Ruby on Rails Guidesには、以下のような説明があります:

config.exceptions_app sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to ActionDispatch::PublicExceptions.new(Rails.public_path).

前節で「(捕捉可能な)すべての種類のエラー」と前置きしましたが、config.exceptions_appは以下においてShowException middlewareまでのレイヤーで発生したエラーのみを捕捉できます。 このことに注意する必要があります。

$ rake middleware
use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007fe32bb2ad70>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Dummy::Application.routes

背景

Rexceptionと同様の機能を持つものとして、@yuki24さんのRambulanceというGemがあります。 Rambulanceはジェネレータを持ち、またコントローラを継承してメソッドを拡張できたりなど、とても素晴らしいGemです。 RexceptionはRambulanceの下位互換のような位置づけですが、その分実装がシンプルなため抵抗なく導入できると思います。

Rexceptionは、元々個人で運営していたサービスのエラーページ実装部分を切り出したものです。 記事の最後に示しますが、実装にあたり参考にしたページで@yuki24さんが多く言及されており、その思想にかなり影響を受けています(貴重な情報に感謝いたします)。

検索ではrescue_fromによる方法が多くヒットしますが、副作用が多いためよい方法とはいえません。 その代替手段のひとつとしての選択肢になれたら嬉しいです。

備考

エラーページを動的に生成する主な理由は、レイアウトの実装が面倒、などの理由だと思います。 ただ、@yuki24さんの指摘の通り、可能であれば/public下に静的ページとして配置した方がよいです。 本格的な運用時の、静的ファイルでエラー表示するまでの代替手段、として考えておいた方がよいと思います。

また、@kennさんも指摘されていますが、メンテナンス画面など、結局動的だけではすべてのケースには対処できません。 このことも踏まえた上で、気をつけてエラーページを実装すべきでしょう。

参考