rescue_from で拾えない例外がある
Rails が用意してくれている rescue_from
は controller の外側で発生した例外を拾ってくれない。
例えばパラメータに不正なエンコーディングが含まれるときに、Rails は ActionController::BadRequest
を例外として投げる。しかし、この処理は Rails の routing 層で行われているため rescue_from
で捕捉することはできない。
そのため Rails の外で発生した例外を捕捉していない場合、ユーザには意図していないエラーページが見えている可能性がある。
Rails の外で起きる例外は exceptions_app で処理するのがお手軽
例えば config/initializers/exceptions_app.rb
に以下のコードを書いておく(ErrorsController と error action は最初に作っておく)。
Rails.configuration.exceptions_app = ->(env) { ErrorsController.action(:error).call(env) }
こうするだけで Rails の外で起きた例外を拾うことができ、Rails アプリ側の ErrorsController で処理を行った結果を返すことが可能になる。
ちなみに env には以下の情報が入っている。
env["action_dispatch.exception"] # 発生した例外
env["action_dispatch.original_path"] # 元のリクエストパス
env["PATH_INFO"] # 発生した例外に紐づくステータスコードのパス ex) /404
# 紐付けは ActionDispatch::ExceptionWrapper 参照
Rails アプリ側の ErrorsController に処理を戻せるので、後はいつもの調子でアプリ側を実装するだけでよい。Rails 側に戻ることで、アプリ側と共通のヘッダー、フッターを用いた動的なエラーページも表示することができる。
exceptions_app の仕組み
ActionDispatch::ShowExceptions
という Rails 組み込みの middleware があり、これが Rails の外側で発生した例外を全て exceptions_app に設定したオブジェクトへ投げてくれる。exceptions_app には rack middleware として扱えるオブジェクトであれば何でも設定できる。上記の例では lambda で包んで rack middleware として扱えるようにしている(コメントで教えていただきましたが、ActionController::Metal.action
が rack application 返すので Rails.configuration.exceptions_app = ErrorsController.action(:show)
でもよいです)。
全ての例外と書いたが厳密には全てではなくて、ActionDispatch::ShowExceptions
より上位に差し込まれている middleware で発生した例外は捕捉できない。
具体的には rake middleware
すると対象の範囲が分かる。例えば手元にあった Rails 4.1 なアプリで rake middleware
してみると、上から9番目に ActionDispatch::ShowExceptions
がある。
9番目なのでそれより上にある middleware で発生したエラーはハンドリングできない。全てハンドリングしたい場合は、RailsでAPIをつくるときのエラー処理 にある middleware 作る方法が参考になる。
ちなみに exceptions_app にはデフォルトでは ActionDispatch::PublicExceptions
が設定されている。この rack middleware は、public/ にある 404.html, 500.html などを例外時に表示するため、exceptions_app を置き換えることでデフォルトの動作がなくなってしまうことにも注意が必要。
$ rake middleware
use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f9699384050>
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 MyApp::Application.routes
記事を書いた経緯
先日話題になっていた以下の記事を同僚の方が見て、rescue_from で拾えない例外のために middleware まで作る必要はあるのか。もっとお手軽な手段を Rails は用意していないのか。という問いかけがあったので、そのとき調べた結果を折角なのでまとめました。
まとめ
rescue_from で拾えない例外には exceptions_app を使うとお手軽。
メリットとしては、
- Rails アプリ側で処理が可能
- よってエラーページもテンプレートを統一したい場合に楽
デメリットとしては、
-
ActionDispatch::ShowExceptions
より上の層で起きた例外を処理できない - exceptions_app を変えることで、例外時に public/{404,422,500}.html を表示するデフォルト動作がなくなる
- その他にも気をつけた方が良さそうなものがありそうです(コメント参照)
デメリットについては、要件次第になるかとは思いますが、一般的なアプリであれば、エラーをキャッチする gem などを入れておけば十分なのではないかなと。例えば Raven-Ruby は最上位層に例外キャッチする middleware を差し込んでくれるのでエラーを把握することはできる。
例として出した ErrorsController は一例なので、参考リンク周りを見てみると色々やり方があるのが分かって面白い。
参考リンク
-
- 例外をまとめて全部外の rack middleware で処理するという発想がすごい。
-
- 特に ErrorsController の action で再度 raise しているのは cool な感じです。
-
- yuki24 さんの作られている gem。exceptions_app を使っていてエラーページ作るのに非常に良さそうです。使いたいと思いつつ、まだ使えてない。。。
-
- Rambulance と似た機能を持っているけど、もっとシンプルな作りになっているそう。
-
- お手軽にということで私の記事では端折ったのですが、rack middleware を作って例外処理をする例が詳しく書いてあります
-
- 記述が少ない
-
- 静的ページでエラーページ作る場合に便利そうです。assets を使って静的エラーページを生成してくれるので、assets 更新時の手間が削減されると思います。
-
関係するクラス: コードが短いのでちょろっと読むのにちょうどよいです。