Hatena::Grouprubyist

うんたらかんたらRuby RSSフィード

2010-03-07rescue_action_in_publicを使用したエラーハンドリング

rescue_action_in_publicを使用したエラーハンドリング

| rescue_action_in_publicを使用したエラーハンドリング - うんたらかんたらRuby を含むブックマーク はてなブックマーク - rescue_action_in_publicを使用したエラーハンドリング - うんたらかんたらRuby

Railscasts - Handling Exceptions


rescue_action_in_publicが紹介されていた。

以前、存在しないpathを指定した際に404を出力させる方法 - うんたらかんたらRuby - Rubyist

で書いた方式とは別に、既存のrescue_action_in_publicをoverrideさせることで実現している。


やり方

# controller
  protected
  def local_request?
    false
  end

  def rescue_action_in_public(exception)
    case exception
    when ActiveRecord::RecordNotFound
      render :file => "#{RAILS_ROOT}/public/404.html", :status => 404
    else
      super
    end
  end

よくわからんのでactionpackのソースを見てみた

local_request?って何よ(まぁメソッド名で想像つくけど)。

/opt/local/lib/ruby/gems/1.8/gems/actionpack-2.3.5/lib/action_controller/rescue.rb

rescue_action_in_public
      # Overwrite to implement public exception handling (for requests
      # answering false to <tt>local_request?</tt>).  By default will call
      # render_optional_error_file.  Override this method to provide more
      # user friendly error messages.
      def rescue_action_in_public(exception) #:doc:
        render_optional_error_file response_code_for_rescue(exception)
      end

コメントにも、overrideする際は、local_request?をfalseにしろと。


render_optional_error_file

一応デフォで呼ばれるrender_optional_error_file

      # Attempts to render a static error page based on the
      # <tt>status_code</tt> thrown, or just return headers if no such file
      # exists. At first, it will try to render a localized static page.
      # For example, if a 500 error is being handled Rails and locale is :da,
      # it will first attempt to render the file at <tt>public/500.da.html</tt>
      # then attempt to render <tt>public/500.html</tt>. If none of them exist,
      # the body of the response will be left empty.
      def render_optional_error_file(status_code)
        status = interpret_status(status_code)
        locale_path = "#{Rails.public_path}/#{status[0,3]}.#{I18n.locale}.html" if I18n.locale
        path = "#{Rails.public_path}/#{status[0,3]}.html"

        if locale_path && File.exist?(locale_path)
          render :file => locale_path, :status => status, :content_type => Mime::HTML
        elsif File.exist?(path)
          render :file => path, :status => status, :content_type => Mime::HTML
        else
          head status
        end
      end

噂のlocal_request?
      # True if the request came from localhost, 127.0.0.1. Override this
      # method if you wish to redefine the meaning of a local request to
      # include remote IP addresses or other criteria.
      def local_request? #:doc:
        request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST
      end

rescue_action_without_handler

local_request?の呼出し元。

      def rescue_action_without_handler(exception)
        log_error(exception) if logger
        erase_results if performed?

        # Let the exception alter the response if it wants.
        # For example, MethodNotAllowed sets the Allow header.
        if exception.respond_to?(:handle_response!)
          exception.handle_response!(response)
        end

        if consider_all_requests_local || local_request?
          rescue_action_locally(exception)
        else
          rescue_action_in_public(exception)
        end
      end

ここでlocal用とそれ以外でエラーのレンダリングを分けている。(動画にも出てくるが一応試してみた。後述。)

どうやら、consider_all_requests_localってのは、

config/environments/development.rb などの各環境で定義している↓らしい。

config.action_controller.consider_all_requests_local = true

つまり、これとlocalhost(127.0.0.1)両方見て判断している。



rescue_action_locally
      # Render detailed diagnostics for unhandled exceptions rescued from
      # a controller action.
      def rescue_action_locally(exception)
        @template.instance_variable_set("@exception", exception)
        @template.instance_variable_set("@rescues_path", RESCUES_TEMPLATE_PATH)
        @template.instance_variable_set("@contents",
          @template.render(:file => template_path_for_local_rescue(exception)))

        response.content_type = Mime::HTML
        render_for_file(rescues_path("layout"),
          response_code_for_rescue(exception))
      end

rescue_action

一応更なる呼出し元。

rescue_action > rescue_action_without_handler > rescue_action_without_handler

      # Exception handler called when the performance of an action raises
      # an exception.
      def rescue_action(exception)
        rescue_with_handler(exception) ||
          rescue_action_without_handler(exception)
      end



個人的には

rescue_action_in_publicは、overrideされることが前提という印象を持った。

local_request?のoverrideも実害無いし、そもそもどのエラーを表示するかっていうのは

environments以下で定義できるので、この方式でいいような気がする。


例外処理なんて当然どこでもやってるんだろうから、ossのコードでもあとで見てみるか。

実践レベルのベストプラクティス欲しいなぁ。既にあるのかな。

あと、気軽にoverrideできるけど、やってokかどうかの判断っつーの難しいなぁ。



おまけ

rescue_action_locallyとrescue_action_in_publicをそれぞれ実行してみた。

local

config.action_controller.consider_all_requests_local = true で実行。

f:id:rochefort:20100307153404j:image


public

config.action_controller.consider_all_requests_local = false かつ

local_request?をoverrideして実行。

f:id:rochefort:20100307153405p:image