ぐらめぬ・ぜぷつぇんのはてダ(2007 to 2011)

2007年~2011年ごろまで はてなダイアリー に書いてた記事を引っ越してきました。

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メソッドで独自のテンプレートを表示可能。

という感じでした。