天天看點

Rails源代碼分析(13):ActionController::Rescue

  • 概要

  如果方法無法按照預期的方式運作的話就會抛出Exception。這些異常可以在捕捉後傳回一個公共頁面(使用者友好的頁面)或者一個開發者檢視頁面(遍布了debug資訊的頁面)。後者已經由Action Controller實作了,前者應該根據應用量身定做。

  public exception預設為傳回一個由error code命名的靜态html頁面,如果沒有這個檔案,将傳回一個空的response和這個error code給浏覽器。      可以override local_request?來識别本地請求,然後override rescue_action_in_public和rescue_action_locally方法處理傳回頁面。

  

  • 源代碼    

  一些常量,包括Error的類别

  1.     LOCALHOST = '127.0.0.1'.freeze
  2.     DEFAULT_RESCUE_RESPONSE = :internal_server_error
  3.     DEFAULT_RESCUE_RESPONSES = {
  4.       'ActionController::RoutingError'             => :not_found,
  5.       'ActionController::UnknownAction'            => :not_found,
  6.       'ActiveRecord::RecordNotFound'               => :not_found,
  7.       'ActiveRecord::StaleObjectError'             => :conflict,
  8.       'ActiveRecord::RecordInvalid'                => :unprocessable_entity,
  9.       'ActiveRecord::RecordNotSaved'               => :unprocessable_entity,
  10.       'ActionController::MethodNotAllowed'         => :method_not_allowed,
  11.       'ActionController::NotImplemented'           => :not_implemented,
  12.       'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
  13.     }
  14.     DEFAULT_RESCUE_TEMPLATE = 'diagnostics'
  15.     DEFAULT_RESCUE_TEMPLATES = {
  16.       'ActionView::MissingTemplate'       => 'missing_template',
  17.       'ActionController::RoutingError'    => 'routing_error',
  18.       'ActionController::UnknownAction'   => 'unknown_action',
  19.       'ActionView::TemplateError'         => 'template_error'
  20.     }

導入Controller

  1.     def self.included(base) #:nodoc:
  2.       base.cattr_accessor :rescue_responses # 增加了rescue_responses變量
  3.       base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
  4.       base.rescue_responses.update DEFAULT_RESCUE_RESPONSES
  5.       base.cattr_accessor :rescue_templates # 增加了rescue_templates變量
  6.       base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE)
  7.       base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES
  8.       base.class_inheritable_array :rescue_handlers # 增加了rescue_handlers變量 使用者處理異常
  9.       base.rescue_handlers = []
  10.       base.extend(ClassMethods)
  11.       base.class_eval do
  12.         alias_method_chain :perform_action, :rescue 
  13.       end
  14.     end

  可以看到入口為perform_action_with_rescue,在這之前看到兩個類public方法:

  1.     module ClassMethods
  2.       def process_with_exception(request, response, exception) #:nodoc:
  3.         new.process(request, response, :rescue_action, exception)
  4.       end
  5.       # Rescue exceptions raised in controller actions.
  6.       #
  7.       # <tt>rescue_from</tt> receives a series of exception classes or class
  8.       # names, and a trailing <tt>:with</tt> option with the name of a method
  9.       # or a Proc object to be called to handle them. Alternatively a block can
  10.       # be given.
  11.       #
  12.       # Handlers that take one argument will be called with the exception, so
  13.       # that the exception can be inspected when dealing with it.
  14.       #
  15.       # Handlers are inherited. They are searched from right to left, from
  16.       # bottom to top, and up the hierarchy. The handler of the first class for
  17.       # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
  18.       # any.
  19.       #
  20.       #   class ApplicationController < ActionController::Base
  21.       #     rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
  22.       #     rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
  23.       #
  24.       #     rescue_from 'MyAppError::Base' do |exception|
  25.       #       render :xml => exception, :status => 500
  26.       #     end
  27.       #
  28.       #     protected
  29.       #       def deny_access
  30.       #         ...
  31.       #       end
  32.       #
  33.       #       def show_errors(exception)
  34.       #         exception.record.new_record? ? ...
  35.       #       end
  36.       #   end
  37.       def rescue_from(*klasses, &block)
  38.         options = klasses.extract_options!
  39.         unless options.has_key?(:with)
  40.           block_given? ? options[:with] = block : raise(ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument.")
  41.         end
  42.         klasses.each do |klass|
  43.           key = if klass.is_a?(Class) && klass <= Exception
  44.             klass.name
  45.           elsif klass.is_a?(String)
  46.             klass
  47.           else
  48.             raise(ArgumentError, "#{klass} is neither an Exception nor a String")
  49.           end
  50.           # Order is important, we put the pair at the end. When dealing with an
  51.           # exception we will follow the documented order going from right to left.
  52.           rescue_handlers << [key, options[:with]]
  53.         end
  54.       end
  55.     end

  其中process_with_exception方法是在Dispatcher裡面調用到的, Dispatcher.rb裡面的failsafe_rescue裡面:

  1.       def failsafe_rescue(exception)
  2.         self.class.failsafe_response(@output, '500 Internal Server Error', exception) do
  3.           if @controller ||= defined?(::ApplicationController) ? ::ApplicationController : Base
  4.             @controller.process_with_exception(@request, @response, exception).out(@output)
  5.           else
  6.             raise exception
  7.           end
  8.         end
  9.       end

  rescue_action方法如下:

  1.       # Exception handler called when the performance of an action raises an exception.
  2.       def rescue_action(exception)
  3.         log_error(exception) if logger
  4.         erase_results if performed?
  5.         # Let the exception alter the response if it wants.
  6.         # For example, MethodNotAllowed sets the Allow header.
  7.         if exception.respond_to?(:handle_response!)
  8.           exception.handle_response!(response)
  9.         end
  10.         if consider_all_requests_local || local_request?
  11.           rescue_action_locally(exception)
  12.         else
  13.           rescue_action_in_public(exception)
  14.         end
  15.       end
  1.       # Render detailed diagnostics for unhandled exceptions rescued from
  2.       # a controller action.
  3.       def rescue_action_locally(exception)
  4.         add_variables_to_assigns
  5.         @template.instance_variable_set("@exception", exception)
  6.         @template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
  7.         @template.send!(:assign_variables_from_controller)
  8.         @template.instance_variable_set
  9.    ("@contents", @template.render_file(template_path_for_local_rescue(exception), false))
  10.      #template_path_for_local_rescue就是include裡的rescue_templates
  11.         response.content_type = Mime::HTML
  12.         render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
  13.       end
  1.       # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).  By
  2.       # default will call render_optional_error_file.  Override this method to provide more user friendly error messages.s
  3.       def rescue_action_in_public(exception) #:doc:
  4.         render_optional_error_file response_code_for_rescue(exception) 
  5.          #根據response_code來查找檔案, response_code_for_rescue就是include裡的rescue_responses,如果有
  6.       end
  7.       # Attempts to render a static error page based on the <tt>status_code</tt> thrown,
  8.       # or just return headers if no such file exists. For example, if a 500 error is 
  9.       # being handled Rails will first attempt to render the file at <tt>public/500.html</tt>. 
  10.       # If the file doesn't exist, the body of the response will be left empty.
  11.       def render_optional_error_file(status_code)
  12.         status = interpret_status(status_code)
  13.         path = "#{Rails.public_path}/#{status[0,3]}.html"
  14.         if File.exist?(path)
  15.           render :file => path, :status => status
  16.         else
  17.           head status
  18.         end
  19.       end

  繼續看 perform_action_with_rescue方法,可以看到核心方法在rescue_action_with_handler和rescue_action裡面

  1.       def perform_action_with_rescue #:nodoc:
  2.         perform_action_without_rescue
  3.       rescue Exception => exception
  4.         rescue_action_with_handler(exception) || rescue_action(exception)
  5.       end
  1.       # Tries to rescue the exception by looking up and calling a registered handler.
  2.       def rescue_action_with_handler(exception)
  3.         if handler = handler_for_rescue(exception) #用rescue_from設定過後,進行查找handler如下
  4.           if handler.arity != 0
  5.             handler.call(exception)
  6.           else
  7.             handler.call
  8.           end
  9.           true # don't rely on the return value of the handler
  10.         end
  11.       end
  1.       def handler_for_rescue(exception)
  2.         # We go from right to left because pairs are pushed onto rescue_handlers
  3.         # as rescue_from declarations are found.
  4.     #rescue_handlers這個變量就是include裡的 base.class_inheritable_array :rescue_handlers
  5.         _, handler = *rescue_handlers.reverse.detect do |klass_name, handler|
  6.           # The purpose of allowing strings in rescue_from is to support the
  7.           # declaration of handler associations for exception classes whose
  8.           # definition is yet unknown.
  9.           #
  10.           # Since this loop needs the constants it would be inconsistent to
  11.           # assume they should exist at this point. An early raised exception
  12.           # could trigger some other handler and the array could include
  13.           # precisely a string whose corresponding constant has not yet been
  14.           # seen. This is why we are tolerant to unknown constants.
  15.           #
  16.           # Note that this tolerance only matters if the exception was given as
  17.           # a string, otherwise a NameError will be raised by the interpreter
  18.           # itself when rescue_from CONSTANT is executed.
  19.           klass = self.class.const_get(klass_name) rescue nil
  20.           klass ||= klass_name.constantize rescue nil
  21.           exception.is_a?(klass) if klass
  22.         end
  23.         case handler
  24.         when Symbol
  25.           method(handler)
  26.         when Proc
  27.           handler.bind(self)
  28.         end
  29.       end

繼續閱讀