Rails / Exception handling

From WhyNotWiki
Jump to: navigation, search

Contents

What does Rails do when you request a path for which there is no matching route?

In Rails 1.1.6, it responded with a HTTP/1.x 404 Not Found error, displaying the contents of public/404.html if it existed.

And everyone was happy. Because that made sense.

The new 1.2 behavior is to raise a nasty RoutingError (which then gets e-mailed to you along with all the other exceptions, if you're using ExceptionNotifier) and then show the user a nasty 500 error page.

This is not the desired behavior for me, for a couple reasons:

  • These types of errors are too easy for a user to trigger. There's no way to stop the user from going to http://yoursite.com/foo and triggering a (what should be a) 404.
    • As such, they are not usually "exceptions" that you care about.
    • As such, these probably-not-a-problem errors should not be e-mailed to the developer with the other errors which are usually much more urgent.
  • The user should receive an appropriate response from HTTP. 500 is not appropriate because that suggests that something went wrong on the server end, which it didn't. "404 Not Found" is the only appropriate response to give, because indeed, Rails was unable to find a matching route.

Apparently I'm not the only one to think this new behavior sucks...

http://brian.pontarelli.com/2007/01/14/handling-rails-404-and-500-errors/. Retrieved on 2007-05-11 11:18.

I spent a couple of hours trying to figure out how to handle 404 and 500 errors in Rails. This is not simple and actually really annoying. Hopefully future versions clean this up because right now it sucks pretty badly

How to fix this behavior in Rails 1.2.3

http://forum.textdrive.com/viewtopic.php?id=16694 suggests doing something like this in ApplicationController:

def rescue_action_in_public(exception)
  case exception
    when ActiveRecord::RecordNotFound, ActionController::RoutingError, ActionController::UnknownController, ActionController::UnknownAction
      #render_404
    else          
      #render_500
  end
end

However, if you are using the exception_notification plugin (and you should be!) (> r7360), then this is handled for you:

./lib/exception_notifiable.rb

    def exceptions_to_treat_as_404
      exceptions = [ActiveRecord::RecordNotFound,
                    ActionController::UnknownController,
                    ActionController::UnknownAction]
      exceptions << ActionController::RoutingError if ActionController.const_defined?(:RoutingError)
      exceptions
    end

    def rescue_action_in_public(exception)
      case exception
        when *self.class.exceptions_to_treat_as_404
          render_404

        else
          render_500

          ...

          ExceptionNotifier.deliver_exception_notification(exception, self,
            request, data)
      end
    end

So install exception_notification and be happy again!

Other good links:



Dissection: What does Rails do when an (otherwise unhandled) exception occurs?

Just look at the source for rescue_action(exception):

    # File vendor/rails/actionpack/lib/action_controller/rescue.rb, line 26
       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

It

  • logs it
  • displays it to the screen (maybe)

Rails makes the following assumptions:

  • During development, you want to see a page with detailed debug info. This is called a "local rescue" or a "rescue for the developer view" ([1]).
  • For everyone else in the world, you want to show the visitor a user-friendly but uninformitive message saying that there was an error (but not giving the details of this error).

To decide whether you are a developer or an "everyone else", it doesn't look at your environment ('development' or 'production'), like I might have expected, but at your IP address (by default, 127.0.0.1 is the only IP considered local by default -- which basically only includes the case when you are doing development with webrick). That could be useful, I suppose, if you were debugging an error remotely and wanted it to automatically show the developer view any time you connected from your home computer. But I'm still not sure I like it.... My opinion:

  • On the production site, the developers are going to want to always be notified about exceptions (via e-mail or whatever).
  • Deciding whether or not you should see extra, developer-only info based on your IP is (1) slightly unsafe, (2) not all that helpful (what if you have a dynamic IP at home or you travel a lot and could be connecting from any number of different IPs?).

See http://api.rubyonrails.org/classes/ActionController/Rescue.html .

I might find it useful to override local_request?() sort of like this:

def local_request?()
  if environment == "production" then false
  if environment == "development" then true
end

I just noticed that something like this is already possible via the environments/* files:

config/environments/development.rb:

# Show full error reports
config.action_controller.consider_all_requests_local = true

That will ensure that all requests done during development will have a local rescue (verbose errors).

(Question: Won't that happen anyway? When would the request be not from localhost?)

(Complaint: Why don't they name it for what it does rather than how it works? It should be called config.action_controller.show_full_error_reports, not config.action_controller.consider_all_requests_local! "local" doesn't mean much to me -- at least not what they think it means.)



Should exceptions be handled the same way during development as during production?

Well, it depends what you want to debug... The default behavior is to show a different error page (rescue_action_locally) during development, one that gives the full error details and a backtrace.

That's usually fine... unless perhaps you're testing the error handling itself and you want it to be identical to production.

In that case, this might be useful... (put this in app/controllers/application.rb)

  def rescue_action_locally(exception)
    rescue_action_in_public(exception)
    super
  end

For instance, right after you install exception_notification in an app, you may want to test that it actually works in development before sending it out into the wild (where it could potentially break horribly)...

Question: will that actually cause it to try to send exception e-mails to someone for every exception that is raised during development?



Errors: What's the difference between "Application error" and "Application error (Rails)"?

"Application error" is found in public/500.html and (I believe) is rendered by ... (??)

"Application error (Rails)", on the other hand, is defined here:

/usr/lib/ruby/gems/1.8/gems/actionpack-1.12.5/lib/action_controller/rescue.rb:

      # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
      def rescue_action_in_public(exception) #:doc:
        case exception
          when RoutingError, UnknownAction then
            render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found")
          else render_text "<html><body><h1>Application error (Rails)</h1></body></html>"
        end
      end

It is also defined here, but this probably doesn't happen as often:

/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/dispatcher.rb

      # If the block raises, send status code as a last-ditch response.
      def failsafe_response(output, status, exception = nil)
        yield
      rescue Object
        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 Object
        end
      end

When would that get called instead of the normal 500 error? I don't know yet...

Normally, it looks like the ugly "Application error (Rails)" message is rendered for all "normal" exceptions (rescue_action_in_public in /usr/lib/ruby/gems/1.8/gems/actionpack-1.12.5/lib/action_controller/rescue.rb). This is surprising; I would have expected it to use 500.rhtml. (Why doesn't it??)

However, if you have the exception_notifiable plugin installed (which I recommend everyone do!), then rescue_action_in_public will be overridden to have the desired behavior. Yay!

./vendor/plugins/exception_notification/lib/exception_notifiable.rb:

  def render_500
    respond_to do |type|
      type.html { render :file => "#{RAILS_ROOT}/public/500.html", :status => "500 Error" }
      type.all  { render :nothing => true, :status => "500 Error" }
    end
  end

  def rescue_action_in_public(exception)
    case exception
      when ActiveRecord::RecordNotFound, ActionController::UnknownController, ActionController::UnknownAction
        render_404

      else
        render_500

        deliverer = self.class.exception_data
        data = case deliverer
          when nil then {}
          when Symbol then send(deliverer)
          when Proc then deliverer.call(self)
        end

        ExceptionNotifier.deliver_exception_notification(exception, self,
          request, data)
    end
  end



How do I set it up to e-mail me when an error message occurs?

See Exception Notifier plugin

How to I change the error pages for my application?

http://wiki.rubyonrails.com/rails/pages/HowtoConfigureTheErrorPageForYourRailsApp

Ads
Personal tools