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.