irohiroki's blog

Ruby, Rails, and Web technologies

Rack middlewareで発生する例外とは

アプリケーションで発生する例外を捕捉し、適切な処理をするのは一般的なことだと思います。コントローラの中で発生する例外はbegin rescueで囲ったり、rescue_fromを使えば捕捉できますが、Rack middlewareで発生する例外はどうでしょうか?

例えばMySQLを使っていてデータベースサーバに接続できない場合、ActiveRecord::ConnectionAdapters::ConnectionManagementというmiddlewareからMysql2::Errorが発生します。

ActiveRecord::ConnectionAdapters::ConnectionManagementはrake middlewareすると10番目に出てきます。

$ rake middleware
  use ActionDispatch::Static
  use Rack::Lock
  use ActiveSupport::Cache::Strategy::LocalCache
  use Rack::Runtime
  use Rails::Rack::Logger
  use ActionDispatch::ShowExceptions
  use ActionDispatch::RemoteIp
  use Rack::Sendfile
  use ActionDispatch::Callbacks
  use ActiveRecord::ConnectionAdapters::ConnectionManagement
  use ActiveRecord::QueryCache
  use ActionDispatch::Cookies
  use ActionDispatch::Session::CookieStore
  use ActionDispatch::Flash
  use ActionDispatch::ParamsParser
  use Rack::MethodOverride
  use ActionDispatch::Head
  use ActionDispatch::BestStandardsSupport
  run MyApp::Application.routes

ウェブサーバはこの上にあり、自分のアプリケーションは一番下です。例外はrescueしなければ上へ上がって行きます。つまり、どうやってもアプリケーションではrescueできません。

ではどうハンドリングすればいいでしょうか?

ActionDispatch::ShowExceptionsを使う?

スタックを見ると上から6番目にActionDispatch::ShowExceptionsというのがあります。ソースを見ると、例外のクラス名をステータスコードにマップしていることがわかります。

例えば、Mysql2::Errorを507 (Insufficient Storage)にマップするだけなら、config/application.rbに下の行を追加し、

ActionDispatch::ShowExceptions.rescue_responses['Mysql2::Error'] = :insufficient_storage

表示したいページをpublic/507.htmlとして置いておけば済みます。

しかしMysql2::Errorの原因が全てInsufficient Storageではありませんし、MySQLが無関係なInsufficient Storageも有りうるので、適切な措置とは言えません。

ActionDispatch::Rescue to the... you know, rescue!!

Rack middlewareで発生した例外はActionDispatch::Rescueというmiddlewareで捕捉できます。ActionDispatch::ShowExceptionsと違い、ActionDispatch::Rescueは例外クラスに対してハンドリングするRackアプリケーションを指定できます。

例えば、application.rbに下のように追加すれば、Mysql2::Errorに対して「Database failure.」と表示できます。

config.middleware.insert_before ActiveRecord::ConnectionAdapters::ConnectionManagement, ActionDispatch::Rescue do
  rescue_from Mysql2::Error, lambda {|env| [500, {'Content-Type' => 'text/plain'}, 'Database failure.'] }
end

ステータスコードは500 (Internal Server Error)で十分でしょう。ポイントは、レスポンスボディとして任意の内容を返せることです。任意の内容を返せるということは、ユーザに適切なメッセージを見せたり、JavaScriptのフロントエンドにJSONなどで適切なステータスを返したりできるということです。

なお、上のrescue_fromはActionDispatch::Rescue#rescue_fromで、コントローラのマクロ風rescue_fromとは別物です。

application.rbに書きたくない?

しかし、前出の

lambda {|env| [500, {'Content-Type' => 'text/plain'}, 'Database failure.'] }

はapplication.rbに書くにはちょっと生々しい印象です。もっとコードらしい場所へ移動するにはどうしたらいいでしょうか。

ActionDispatch::Rescue#rescue_fromの第2引数はRackアプリケーションですから、Rackアプリケーションとして振舞うクラスを指定してやれば済みます。例えばActionController::Metalを継承すればそういうクラスを簡単に作れます。

class DatabaseFailure < ActionController::Metal
  def self.call(env)
    action(:respond).call(env)
  end

  def respond
    self.status = 500
    self.content_type = 'text/plain'
    self.response_body = 'Database failure.'
  end
end

application.rbには下のようになります。

config.middleware.insert_before ActiveRecord::ConnectionAdapters::ConnectionManagement, ActionDispatch::Rescue do
  rescue_from Mysql2::Error, DatabaseFailure
end

Published on 15/06/2011 at 23h25 under . Tags

0 comments

Powered by Typo – Thème Frédéric de Villamil | Photo L. Lemos