404s in Ruby on Rails

One of the things that has been irking me while using Ruby on Rails is the difficulty involved in returning a 404 page. For the most part RoR does a good job of Doing-the-Right-ThingTM when encountering an exception, for example, it converts RecordNotFound and RoutingErrors into 404 pages in production mode; however, there are times when I (the programmer) know that a page doesn't exist but RoR has not yet raised an exception. In such cases, it feels truly wrong to simply raise a RecordNotFound exception, as the semantics are questionable and sometimes I'd rather see my 404 page rather than the traceback page, even in development mode. I've found an example of how to coerce Rails to exhibit the desired behaviour; however, it's kind of messy in that it forces every request to be considered public. The following is a far cleaner solution.

class HttpStatus < Exception
  attr_reader :status
  attr_reader :template

  def initialize(status, template)
      @status = status
      @template = template
  end
end

class Http404 < HttpStatus
  def initialize
      super(404, "#{RAILS_ROOT}/public/404.html")
  end
end

class ApplicationController < ActionController::Base
  protected
      def rescue_action(exception)
          if exception.is_a? HttpStatus
              render :status => exception.status, :file => exception.template
          else
              super
          end
      end
end

Essentially, we override ApplicationController::rescue_action to special case exceptions derived from HttpStatus to set the response status code and render a template. This solution is ideal as it works with both public and local requests and does not interfere with the handling of existing exceptions which already have their own meanings.

The mechanics of how it works are fairly simple: rescue_action is the method that dispatches between rescue_action_in_public and rescue_action_locally, thus by overriding rescue_action rather than either rescue_action_in_public or rescue_action_locally we can define the behaviour of rescue independant of whether or not a request is considered local and thus avoid messing with either local_request? or consider_all_requests_local. Furthermore, by defining our own class of exceptions to invoke the special processing in rescue_action, the applications behaviour is only changed when handling the custom exception, rather than overloading the meaning of existing exceptions.