👌

Rambulanceでお手軽かつ柔軟にRailsの動的エラーページを作る

2023/12/18に公開

はじめに

本記事はプロもくチャット Advent Calendar 2023の18日目です。

https://qiita.com/advent-calendar/2023/puromoku

Railsには標準でエラーページのviewが用意されていますが、このように味気ないものになっています。

デフォルトのエラーページ

本番用のアプリにはこのようなデフォルトページではなく、オリジナルのエラーページを作成するケースがほとんどかと思います。

エラーページの実装方法には、静的なページにする方法と動的なページにする方法があります。

静的なエラーページ

public/404.htmlなどを編集すれば良いだけです。

静的ページで事足りるのであれば、基本的には静的ページを使用した方が良いです。

動的なエラーページ

ApplicationControllerrescue_fromを設定して例外補足する方法が多く紹介されていますが、これはこれで問題があります(ここは今回の本筋ではないので詳細は割愛します)。

そこで本記事では、動的エラーページのおすすめとして、Rambulanceというgemを使う方法を紹介します。

本記事でやること

  1. Rambulanceを導入して、ActionController::RoutingErrorActiveRecord::RecordNotFoundが発生した場合に表示する404のカスタムエラーページを作る
  2. ActionController::RoutingErrorActiveRecord::RecordNotFoundが発生した場合に、それぞれ別の404ページを表示する

手順

Rambulance導入

まずはRambulanceをインストールするため、Gemfileに以下を追加して、bundle installを実行します。

Gemfile
gem 'rambulance'

次に以下を実行します。

$ rails g rambulance:install

このコマンドにより、以下のファイルが生成されます。

  • 各エラーページのviewファイル
    • app/views/errors/bad_request.html.erb
    • app/views/errors/forbidden.html.erb
    • app/views/errors/internal_server_error.html.erb
    • app/views/errors/not_found.html.erb
    • app/views/errors/unprocessable_entity.html.erb
  • エラーページに使用されるレイアウト
    • app/views/layouts/error.html.erb
  • Rambulanceの設定ファイル
    • config/initializers/rambulance.rb

この時点で、生成したviewに対応するステータスコードのエラーを発生させると、それぞれのエラーページを表示することができます。
もちろん、生成したviewのHTML/CSSを編集すればその通りに表示されます。

詳細設定

上記により動的なカスタムエラーページを表示することができますが、config/initializers/rambulance.rbを編集することでさらに詳細な設定を行うことができます。

config/initializers/rambulance.rbはデフォルトで以下のようになっています。

config/initializers/rambulance.rb
Rambulance.setup do |config|
  # 例外クラスとステータスコードの組み合わせ(1)
  config.rescue_responses = {}

  # 使用するレイアウト(2)
  config.layout_name = "error"

  # カスタムエラーのviewファイルを格納するディレクトリ(3)
  config.view_path = "errors"
end

この場合、Rambulanceの設定は以下のような設定になっています。

  1. 例外クラスと、その例外が起こった時に返すステータスコードの組み合わせはデフォルトのまま(追加設定なし)
  2. カスタムエラーのレイアウトにはapp/views/layouts/error.html.erbを使用する
  3. カスタムエラーのviewファイルはapp/views/errorsディレクトリに格納されているものを使う

2, 3についてはそのままなので説明は割愛します。

1の例外クラスとステータスコードの組み合わせは、デフォルトでは以下のようになっています。

ActionController::RoutingError                     => :not_found,
AbstractController::ActionNotFound                 => :not_found,
ActionController::MethodNotAllowed                 => :method_not_allowed,
ActionController::UnknownHttpMethod                => :method_not_allowed,
ActionController::NotImplemented                   => :not_implemented,
ActionController::UnknownFormat                    => :not_acceptable,
ActionDispatch::Http::MimeNegotiation::InvalidType => :not_acceptable,
ActionController::MissingExactTemplate             => :not_acceptable,
ActionController::InvalidAuthenticityToken         => :unprocessable_entity,
ActionController::InvalidCrossOriginRequest        => :unprocessable_entity,
ActionDispatch::Http::Parameters::ParseError       => :bad_request,
ActionController::BadRequest                       => :bad_request,
ActionController::ParameterMissing                 => :bad_request,
Rack::QueryParser::ParameterTypeError              => :bad_request,
Rack::QueryParser::InvalidParameterError           => :bad_request,
ActiveRecord::RecordNotFound                       => :not_found,
ActiveRecord::StaleObjectError                     => :conflict,
ActiveRecord::RecordInvalid                        => :unprocessable_entity,
ActiveRecord::RecordNotSaved                       => :unprocessable_entity

これに加えて、例えばActiveRecord::RecordNotUniqueの例外が発生した時にunprocessable_entity.html.erbのエラーページを表示させたいとします。
その場合は、以下のように例外クラス名のStringをキー、ステータスコードのエイリアスを値として設定を追加しましょう。

config/initializers/rambulance.rb
Rambulance.setup do |config|
  # 例外クラスとステータスコードの組み合わせ
-  config.rescue_responses = {}
+  config.rescue_responses = {
+    "ActiveRecord::RecordNotUnique" => :unprocessable_entity
+}

  # 使用するレイアウト
  config.layout_name = "error"

  # カスタムエラーのviewファイルを格納するディレクトリ
  config.view_path = "errors"
end

ステータスコードを拡張する

上記の方法で、カスタムエラーページを作成することができました。

基本的には各ステータスコードに対応するviewファイルは最大1つとなっています。

しかし、同じステータスコードでもエラー内容によってエラーページを出し分けたいという場合もあるかもしれません。
例えば以下のような場合です。

  • ActiveRecord::RecordNotFoundが発生した場合は404エラーとしてrecord_not_found.html.erbを表示する
  • それ以外の404系のエラー(ActionController::RoutingErrorなど)が発生した場合はnot_found.html.erbを表示する

Rambulanceでは、ステータスコードのエイリアスとエラーページのviewファイル名が一致している必要があります。
そして404のエイリアスは:not_foundであり、:record_not_foundというエイリアスは存在しません。
したがって、現状では404エラーとして表示できるのはnot_found.html.erbのみです。

このエイリアスはRackの組み込みとして定義されており、他の値を使うことはできません。
具体的には、エイリアスとして以下のものが用意されています。
https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L475-L543

各ステータスコードのエイリアス
{
  :continue=>100,
  :switching_protocols=>101,
  :processing=>102,
  :early_hints=>103,
  :ok=>200,
  :created=>201,
  :accepted=>202,
  :non_authoritative_information=>203,
  :no_content=>204,
  :reset_content=>205,
  :partial_content=>206,
  :multi_status=>207,
  :already_reported=>208,
  :im_used=>226,
  :multiple_choices=>300,
  :moved_permanently=>301,
  :found=>302,
  :see_other=>303,
  :not_modified=>304,
  :use_proxy=>305,
  :temporary_redirect=>307,
  :permanent_redirect=>308,
  :bad_request=>400,
  :unauthorized=>401,
  :payment_required=>402,
  :forbidden=>403,
  :not_found=>404,
  :method_not_allowed=>405,
  :not_acceptable=>406,
  :proxy_authentication_required=>407,
  :request_timeout=>408,
  :conflict=>409,
  :gone=>410,
  :length_required=>411,
  :precondition_failed=>412,
  :content_too_large=>413,
  :uri_too_long=>414,
  :unsupported_media_type=>415,
  :range_not_satisfiable=>416,
  :expectation_failed=>417,
  :misdirected_request=>421,
  :unprocessable_content=>422,
  :locked=>423,
  :failed_dependency=>424,
  :too_early=>425,
  :upgrade_required=>426,
  :precondition_required=>428,
  :too_many_requests=>429,
  :request_header_fields_too_large=>431,
  :unavailable_for_legal_reasons=>451,
  :internal_server_error=>500,
  :not_implemented=>501,
  :bad_gateway=>502,
  :service_unavailable=>503,
  :gateway_timeout=>504,
  :http_version_not_supported=>505,
  :variant_also_negotiates=>506,
  :insufficient_storage=>507,
  :loop_detected=>508,
  :network_authentication_required=>511
}

以下のような関連付けを行なったところで、record_not_found.html.erbは表示できません。

config/initializers/rambulance.rb
  config.rescue_responses = {
+    'ActiveRecord::RecordNotFound' => :record_not_found
  }

上述のように、:record_not_foundというエイリアスは用意されていないためです。

逆に考えると、ステータスコードのエイリアスを拡張して:record_not_foundを定義できれば、エラーページにも使用することができます。

以下の記述を追加することで、404エラーに対応するエイリアスとして:record_not_foundを定義することができます。

config/initializers/rambulance.rb
+  Rack::Utils::SYMBOL_TO_STATUS_CODE[:record_not_found] = 404
  
  config.rescue_responses = {
    'ActiveRecord::RecordNotFound' => :record_not_found
  }

これにより、以下のような404エラーページの出し分けが可能になります。

  • ActiveRecord::RecordNotFoundが発生した場合は404エラーとしてrecord_not_found.html.erbを表示する
  • それ以外の404系のエラー(ActionController::RoutingErrorなど)が発生した場合はnot_found.html.erbを表示する

まとめ

Rambulanceを使うことで手軽に動的なエラーページを作成できます。

また、ステータスコードのエイリアスを拡張することで、エラークラスとエラーページの関連付けをより柔軟に行うことができます。

ただ大前提として、静的ページで事足りるのであれば静的ページを使うようにした方が良いです。

それでも動的ページが必要な場合は、ぜひこの方法をお試しください!

Discussion