Rails の rescue_from で拾えない例外を exceptions_app で処理する

  • 232
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

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 は用意していないのか。という問いかけがあったので、そのとき調べた結果を折角なのでまとめました。

RailsでAPIをつくるときのエラー処理

まとめ

rescue_from で拾えない例外には exceptions_app を使うとお手軽。
メリットとしては、

  • Rails アプリ側で処理が可能
  • よってエラーページもテンプレートを統一したい場合に楽

デメリットとしては、

  • ActionDispatch::ShowExceptions より上の層で起きた例外を処理できない
  • exceptions_app を変えることで、例外時に public/{404,422,500}.html を表示するデフォルト動作がなくなる
  • その他にも気をつけた方が良さそうなものがありそうです(コメント参照)

デメリットについては、要件次第になるかとは思いますが、一般的なアプリであれば、エラーをキャッチする gem などを入れておけば十分なのではないかなと。例えば Raven-Ruby は最上位層に例外キャッチする middleware を差し込んでくれるのでエラーを把握することはできる。

例として出した ErrorsController は一例なので、参考リンク周りを見てみると色々やり方があるのが分かって面白い。

参考リンク