500系のエラー処理のハンドリングについて
ハイ、本日最後のエントリー。500系が発生する時、ちゃんと捕捉できるのか?ということで、少し調べてみました。
参考:あきおの日記
というわけでまたもやソースコード探索いっきまーす\(゚∀゚)/
あ、使用しているRailsは1.2.6です。予め。
rails-1.2.6/lib/dispatcher.rb :
class Dispatcher class << self # ...これがよく分からない。selfはDispatcherクラスになるのかなあ?なんでわざわざ? # ↓outputは$stdoutになります。 def dispatch(...) controller = nil if cgi ||= new_cgi(output) request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi) prepare_application controller = ActionController::Routing::Routes.recognize(request) controller.process(request, response).out(output) end rescue Exception => exception # errors from CGI dispatch #早速ヒット! failsafe_response(output, '500 Internal Server Error', exception) do controller ||= (ApplicationController rescue ActionController::Base) controller.process_with_exception(request, response, exception).out(output) end ensure # 省略。 end
早速ヒットしちゃいました。failsafe_responseにはブロックを渡しているので、内部ではyieldされていることが予想されます。また、出力先であるoutputが渡っていきます。
rails-1.2.6/lib/dispatcher.rb : (引き続き)
def failsafe_response(output, status, exception = nil) yield rescue Exception # errors from executed block begin output.write "Status: #{status}\r\n" if exception message = exception.to_s + "\r\n" + exception.backtrace.join("\r\n") error_path = File.join(RAILS_ROOT, 'public', '500.html') if defined?(RAILS_DEFAULT_LOGGER) && !RAILS_DEFAULT_LOGGER.nil? RAILS_DEFAULT_LOGGER.fatal(message) output.write "Content-Type: text/html\r\n\r\n" if File.exists?(error_path) output.write(IO.read(error_path)) else output.write("<html><body><h1>Application error (Rails)</h1></body></html>") end else output.write "Content-Type: text/plain\r\n\r\n" output.write(message) end end rescue Exception # Logger or IO errors end end
えーっと・・・渡されたブロックを実行して、その中で例外が発生したら、とにもかくにも何か出力しようとしてます。
で、この「渡されたブロック」というのは
do controller ||= (ApplicationController rescue ActionController::Base) controller.process_with_exception(request, response, exception).out(output) end
になります。メソッド名からして、例外処理です。それもActionController::BaseかApplicationControllerのprocess_with_exceptionメソッドです。
つまり、例外処理の中でさらに例外が発生してしまったら、それはもうどうしようもないのでデフォルトでとにかく出力しようとしているわけです。
えっと・・・とりあえず、process_with_exceptionの方を見ていきます。いや、だって更にその中の例外までどうこうしようというのはきつすぎです。
で、process_with_exceptionメソッドの方ですがgrepしたところ妙なところで定義されていました。
actionpack-1.13.6/lib/action_controller/rescue.rb :
module ActionController #... module Rescue #... module ClassMethods #:nodoc: #... def process_with_exception(request, response, exception) new.process(request, response, :rescue_action, exception) end
うーっと、なぜコレが前掲のコードで呼ばれるのか正直よく分かりません。とまれ、いろいろpとかloggerとか入れてみたところ呼ばれているのは確かです。
newというのは、おそらくControllerの事だと思います。ですので、rescue_actionというアクションメソッドをここで呼んでいます。exceptionは最初に発生した例外が引き継がれてます。
で、rescue_actionというのがこれまた、ActionController::Rescueで定義されているのです。正直よく分かりません。
def rescue_action(exception) log_error(exception) if logger erase_results if performed? if consider_all_requests_local || local_request? rescue_action_locally(exception) else rescue_action_in_public(exception) end end
consider_all_requests_localというのはenvironments/xxxx.rbで
config.action_controller.consider_all_requests_local = false
とかで定義している値です・・・多分。local_request?というのは、多分本とかによるとこれをApplicationControllerでfalse返しで上書きすることで、開発環境モードでも本番用のエラー処理を動かせるようです。
つまり、設定値とApplicationControllerの両方がfalseじゃないと本番環境用のエラー処理(rescue_action_in_public)は動かないと言うことになります。
通常の開発環境ではrescue_action_locallyが動くようです。ちなみにlog_error()というのはexceptionからスタックトレースをログ出力しているようです。
どうでもいいですが、local_request?のデフォルトの実装が
def local_request? #:doc: [request.remote_addr, request.remote_ip] == ["127.0.0.1"] * 2 end
というのも何だかへぇな感じです。
で、開発環境で動かしていて例えばRubyの文法ミスとかした時に、なんだかリクエストやヘッダーなど含め、ミスしているファイル名や行数まで表示している親切な画面ありますよね?
あれを出力しているのが、rescue_action_locallyメソッドになり、ActionPackに同梱されているRHTMLを出力しているわけです。
一方一般公開する環境ではそれを表示してはマズイ、ということで、rescue_action_in_publicメソッドでは
def rescue_action_in_public(exception) #:doc: case exception when RoutingError, UnknownAction render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found") else render_text(IO.read(File.join(RAILS_ROOT, 'public', '500.html')), "500 Internal Error") end end
として、ここでようやく、railsアプリのpublic/{404|500}.htmlが出力されるわけです。長かったー。
まぁ簡単にまとめると、
- 例外が発生したら、public/{404|500}.htmlを表示さえできればオッケー
- → public/{404|500}.htmlをカスタマイズ。
- 独自の業務ログを出力したりといった処理を入れたい。
- → ApplicationControllerなどでrescue_action_in_publicメソッドをオーバーライド。
- → renderメソッドで独自のテンプレートを表示可能。
という感じでした。