516

I'd like to 'fake' a 404 page in Rails. In PHP, I would just send a header with the error code as such:

header("HTTP/1.0 404 Not Found");

How is that done with Rails?

Yuval Karmi
  • 26,277
  • 39
  • 124
  • 175

12 Answers12

1098

Don't render 404 yourself, there's no reason to; Rails has this functionality built in already. If you want to show a 404 page, create a render_404 method (or not_found as I called it) in ApplicationController like this:

def not_found
  raise ActionController::RoutingError.new('Not Found')
end

Rails also handles AbstractController::ActionNotFound, and ActiveRecord::RecordNotFound the same way.

This does two things better:

1) It uses Rails' built in rescue_from handler to render the 404 page, and 2) it interrupts the execution of your code, letting you do nice things like:

  user = User.find_by_email(params[:email]) or not_found
  user.do_something!

without having to write ugly conditional statements.

As a bonus, it's also super easy to handle in tests. For example, in an rspec integration test:

# RSpec 1

lambda {
  visit '/something/you/want/to/404'
}.should raise_error(ActionController::RoutingError)

# RSpec 2+

expect {
  get '/something/you/want/to/404'
}.to raise_error(ActionController::RoutingError)

And minitest:

assert_raises(ActionController::RoutingError) do 
  get '/something/you/want/to/404'
end

OR refer more info from Rails render 404 not found from a controller action

Steven Soroka
  • 19,404
  • 4
  • 52
  • 40
  • In test/unit, I believe it's something like: assert_raises(ActionController::RoutingError) do get '/something/you/want/to/404' end – Steven Soroka Aug 26 '11 at 05:44
  • 3
    There is a reason to do it yourself. If your application hijacks all of the routes from the root. It's bad design, but sometimes un-avoidable. – ablemike Oct 03 '11 at 13:54
  • Do not use render_404 and return if params[:something].blank? – Boris Barroso Mar 09 '12 at 20:02
  • 10
    This approach also lets you use the ActiveRecord bang finders (find!, find_by_...!, etc.), which all raise an ActiveRecord::RecordNotFound exception if no record is found (triggering the rescue_from handler). – gjvis Jun 18 '12 at 17:59
  • 2
    This raises a 500 Internal Server Error for me, not a 404. What am I missing? – Glenn Jul 29 '12 at 21:57
  • I just love short, efficient, cool and beautiful answer! You own your +252 vote. – Kulgar Oct 11 '12 at 15:27
  • 1
    Being a Rails newb I found this doesn't fire if I use a generic .find(), but does work if I do .find_by_id() for example. – Sebastian Patten Oct 29 '12 at 12:08
  • 1
    This returns a 404 to the client, but logs a 500. Is there an easy way to log a 404? – Ross Dec 09 '12 at 19:10
  • Not sure if this has anything to do with it, but rails has a bug related to not handling missing templates properly: https://github.com/rails/rails/issues/4127 – thekingoftruth Jan 17 '13 at 21:14
  • 5
    Seems like `ActionController::RecordNotFound` is the better option? – Peter Ehrlich Feb 04 '13 at 04:59
  • Not sure I like this, as in testing, I generally like to do something like... get :blah; assert_response :not_found; – Subimage Apr 25 '13 at 05:10
  • 1
    this isn't good, it returns error 500 and shows up in airbrake/rollbar. – mrbrdo Jul 04 '13 at 14:28
  • 2
    Assuming this answer is the Railsy way: I hate how Rails cannot stick to one paradigm: so you raise an exception to get a 404, but you stick to render :status for everything else, say a 401 or 403? This just doesn't make sense. – radiospiel Aug 05 '13 at 08:25
  • 5
    The code worked great but the test didn't until I realized I was using RSpec 2 which has different syntax: `expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)` /via http://stackoverflow.com/a/1722839/993890 – ryanttb Nov 05 '13 at 19:55
  • 2
    If you just want to emulate the default behaviour ```raise ActiveRecord::RecordNotFound``` – bonyiii Jul 02 '14 at 16:06
  • I've merged the multiple answers in one Gist: https://gist.github.com/nunommc/ee3c2596b1acbce50818 – Nuno Costa Sep 11 '14 at 11:40
  • 2
    Is this still true? At least in development, I get an error page when my controller throws ActiveRecord::RecordNotFound. – Robin Clowers Oct 18 '14 at 19:19
  • 2
    Now it's better practice to use fail: `fail ActiveRecord::RecordNotFound, 'Not Found'` – David Morales Feb 09 '15 at 18:56
  • I'm testing with regular rails tests (not rspec). The following works: ```assert_raise ActiveRecord::RecordNotFound do get :download, :id => 999 end``` Strangely though, if I ```assert_response :missing```then that fails. I get the message ```'expected response to be a , but was <200>```. In actual use, I do get a 404. – Confused Vorlon Dec 28 '15 at 16:09
  • You can also have `# RSpec 2+ expect { get '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError).with_message('Not Found') ` if you want to make sure you're getting the Not Found message – Michael Brawn May 03 '16 at 17:53
  • Thanks for your answer. The app I work with use `friendly_id` gem. Using find and find_by would not work. Instead use `exists?` method. This gist shows how to https://gist.github.com/AhmedNadar/b450fd65eda9c4afb8e04e28f1348af6 – egyamado Jan 16 '17 at 05:05
  • Rails redirect to **404** if `find` raise **ActiveRecord::RecordNotFound** – maguri Mar 16 '18 at 12:20
  • Remember, if you want to see this working on a development environment, you have to 'change config.consider_all_requests_local' to 'false' on 'config/environments/development.rb' – alex Jun 10 '19 at 19:56
265

HTTP 404 Status

To return a 404 header, just use the :status option for the render method.

def action
  # here the code

  render :status => 404
end

If you want to render the standard 404 page you can extract the feature in a method.

def render_404
  respond_to do |format|
    format.html { render :file => "#{Rails.root}/public/404", :layout => false, :status => :not_found }
    format.xml  { head :not_found }
    format.any  { head :not_found }
  end
end

and call it in your action

def action
  # here the code

  render_404
end

If you want the action to render the error page and stop, simply use a return statement.

def action
  render_404 and return if params[:something].blank?

  # here the code that will never be executed
end

ActiveRecord and HTTP 404

Also remember that Rails rescues some ActiveRecord errors, such as the ActiveRecord::RecordNotFound displaying the 404 error page.

It means you don't need to rescue this action yourself

def show
  user = User.find(params[:id])
end

User.find raises an ActiveRecord::RecordNotFound when the user doesn't exist. This is a very powerful feature. Look at the following code

def show
  user = User.find_by_email(params[:email]) or raise("not found")
  # ...
end

You can simplify it by delegating to Rails the check. Simply use the bang version.

def show
  user = User.find_by_email!(params[:email])
  # ...
end
Bob Aman
  • 32,839
  • 9
  • 71
  • 95
Simone Carletti
  • 173,507
  • 49
  • 363
  • 364
61

The newly Selected answer submitted by Steven Soroka is close, but not complete. The test itself hides the fact that this is not returning a true 404 - it's returning a status of 200 - "success". The original answer was closer, but attempted to render the layout as if no failure had occurred. This fixes everything:

render :text => 'Not Found', :status => '404'

Here's a typical test set of mine for something I expect to return 404, using RSpec and Shoulda matchers:

describe "user view" do
  before do
    get :show, :id => 'nonsense'
  end

  it { should_not assign_to :user }

  it { should respond_with :not_found }
  it { should respond_with_content_type :html }

  it { should_not render_template :show }
  it { should_not render_with_layout }

  it { should_not set_the_flash }
end

This healthy paranoia allowed me to spot the content-type mismatch when everything else looked peachy :) I check for all these elements: assigned variables, response code, response content type, template rendered, layout rendered, flash messages.

I'll skip the content type check on applications that are strictly html...sometimes. After all, "a skeptic checks ALL the drawers" :)

http://dilbert.com/strips/comic/1998-01-20/

FYI: I don't recommend testing for things that are happening in the controller, ie "should_raise". What you care about is the output. My tests above allowed me to try various solutions, and the tests remain the same whether the solution is raising an exception, special rendering, etc.

Jaime Bellmyer
  • 23,051
  • 7
  • 53
  • 50
  • 3
    really like this answer, especially with regards to the testing of the output and not the methods called in the controller… – xentek Mar 28 '13 at 20:38
  • 1
    Rails has built-in 404 status: `render :text => 'Not Found', :status => :not_found`. – Lasse Bunk Dec 28 '13 at 13:17
  • 1
    @JaimeBellmyer - I'm certain it does *not* return a 200 when you're in a deployed (i.e. staging / prod) environment. I do this in several applications and it works as described in the accepted solution. Perhaps what you're referring to is that it returns a 200 when it renders the debug screen in development where you probably have the ```config.consider_all_requests_local``` parameter set to true in your ```environments/development.rb``` file. If you raise an error, as described in the accepted solution, in staging/production, you will definitely get a 404, not a 200. – Javid Jamae Jul 30 '14 at 19:01
22

You could also use the render file:

render file: "#{Rails.root}/public/404.html", layout: false, status: 404

Where you can choose to use the layout or not.

Another option is to use the Exceptions to control it:

raise ActiveRecord::RecordNotFound, "Record not found."
Paulo Fidalgo
  • 21,709
  • 7
  • 99
  • 115
16

The selected answer doesn't work in Rails 3.1+ as the error handler was moved to a middleware (see github issue).

Here's the solution I found which I'm pretty happy with.

In ApplicationController:

  unless Rails.application.config.consider_all_requests_local
    rescue_from Exception, with: :handle_exception
  end

  def not_found
    raise ActionController::RoutingError.new('Not Found')
  end

  def handle_exception(exception=nil)
    if exception
      logger = Logger.new(STDOUT)
      logger.debug "Exception Message: #{exception.message} \n"
      logger.debug "Exception Class: #{exception.class} \n"
      logger.debug "Exception Backtrace: \n"
      logger.debug exception.backtrace.join("\n")
      if [ActionController::RoutingError, ActionController::UnknownController, ActionController::UnknownAction].include?(exception.class)
        return render_404
      else
        return render_500
      end
    end
  end

  def render_404
    respond_to do |format|
      format.html { render template: 'errors/not_found', layout: 'layouts/application', status: 404 }
      format.all { render nothing: true, status: 404 }
    end
  end

  def render_500
    respond_to do |format|
      format.html { render template: 'errors/internal_server_error', layout: 'layouts/application', status: 500 }
      format.all { render nothing: true, status: 500}
    end
  end

and in application.rb:

config.after_initialize do |app|
  app.routes.append{ match '*a', :to => 'application#not_found' } unless config.consider_all_requests_local
end

And in my resources (show, edit, update, delete):

@resource = Resource.find(params[:id]) or not_found

This could certainly be improved, but at least, I have different views for not_found and internal_error without overriding core Rails functions.

Bob Aman
  • 32,839
  • 9
  • 71
  • 95
Augustin Riedinger
  • 20,909
  • 29
  • 133
  • 206
  • 3
    this is a very nice solution; however, you dont need the `|| not_found` part, just call `find!` (notice the bang) and it will throw ActiveRecord::RecordNotFound when the resource cannot be retrieved. Also, add ActiveRecord::RecordNotFound to the array in the if condition. – Marek Příhoda Apr 13 '14 at 23:12
  • 1
    I would rescue `StandardError` and not `Exception`, just in case. Actually I'll leave standard 500 static page and not use custom `render_500` at all, meaning I'll explicitly `rescue_from` array of errors related to 404 – Dr.Strangelove Aug 12 '16 at 06:46
7

these will help you...

Application Controller

class ApplicationController < ActionController::Base
  protect_from_forgery
  unless Rails.application.config.consider_all_requests_local             
    rescue_from ActionController::RoutingError, ActionController::UnknownController, ::AbstractController::ActionNotFound, ActiveRecord::RecordNotFound, with: lambda { |exception| render_error 404, exception }
  end

  private
    def render_error(status, exception)
      Rails.logger.error status.to_s + " " + exception.message.to_s
      Rails.logger.error exception.backtrace.join("\n") 
      respond_to do |format|
        format.html { render template: "errors/error_#{status}",status: status }
        format.all { render nothing: true, status: status }
      end
    end
end

Errors controller

class ErrorsController < ApplicationController
  def error_404
    @not_found_path = params[:not_found]
  end
end

views/errors/error_404.html.haml

.site
  .services-page 
    .error-template
      %h1
        Oops!
      %h2
        404 Not Found
      .error-details
        Sorry, an error has occured, Requested page not found!
        You tried to access '#{@not_found_path}', which is not a valid page.
      .error-actions
        %a.button_simple_orange.btn.btn-primary.btn-lg{href: root_path}
          %span.glyphicon.glyphicon-home
          Take Me Home
Abdullah
  • 2,015
  • 2
  • 20
  • 29
Caner
  • 1,448
  • 23
  • 38
4
routes.rb
  get '*unmatched_route', to: 'main#not_found'

main_controller.rb
  def not_found
    render :file => "#{Rails.root}/public/404.html", :status => 404, :layout => false
  end
3
<%= render file: 'public/404', status: 404, formats: [:html] %>

just add this to the page you want to render to the 404 error page and you are done.

Ahmed Reza
  • 293
  • 1
  • 3
  • 19
1

I wanted to throw a 'normal' 404 for any logged in user that isn't an admin, so I ended up writing something like this in Rails 5:

class AdminController < ApplicationController
  before_action :blackhole_admin

  private

  def blackhole_admin
    return if current_user.admin?

    raise ActionController::RoutingError, 'Not Found'
  rescue ActionController::RoutingError
    render file: "#{Rails.root}/public/404", layout: false, status: :not_found
  end
end
Petercopter
  • 1,218
  • 11
  • 16
1

Raising ActionController::RoutingError('not found') has always felt a little bit strange to me - in the case of an unauthenticated user, this error does not reflect reality - the route was found, the user is just not authenticated.

I happened across config.action_dispatch.rescue_responses and I think in some cases this is a more elegant solution to the stated problem:

# application.rb
config.action_dispatch.rescue_responses = {
  'UnauthenticatedError' => :not_found
}

# my_controller.rb
before_action :verify_user_authentication

def verify_user_authentication
  raise UnauthenticatedError if !user_authenticated?
end

What's nice about this approach is:

  1. It hooks into the existing error handling middleware like a normal ActionController::RoutingError, but you get a more meaningful error message in dev environments
  2. It will correctly set the status to whatever you specify in the rescue_responses hash (in this case 404 - not_found)
  3. You don't have to write a not_found method that needs to be available everywhere.
Dharman
  • 30,962
  • 25
  • 85
  • 135
Mark G.
  • 3,176
  • 1
  • 27
  • 29
0

To test the error handling, you can do something like this:

feature ErrorHandling do
  before do
    Rails.application.config.consider_all_requests_local = false
    Rails.application.config.action_dispatch.show_exceptions = true
  end

  scenario 'renders not_found template' do
    visit '/blah'
    expect(page).to have_content "The page you were looking for doesn't exist."
  end
end
Marek Příhoda
  • 11,108
  • 3
  • 39
  • 53
0

If you want to handle different 404s in different ways, consider catching them in your controllers. This will allow you to do things like tracking the number of 404s generated by different user groups, have support interact with users to find out what went wrong / what part of the user experience might need tweaking, do A/B testing, etc.

I have here placed the base logic in ApplicationController, but it can also be placed in more specific controllers, to have special logic only for one controller.

The reason I am using an if with ENV['RESCUE_404'], is so I can test the raising of AR::RecordNotFound in isolation. In tests, I can set this ENV var to false, and my rescue_from would not fire. This way I can test the raising separate from the conditional 404 logic.

class ApplicationController < ActionController::Base

  rescue_from ActiveRecord::RecordNotFound, with: :conditional_404_redirect if ENV['RESCUE_404']

private

  def conditional_404_redirect
    track_404(@current_user)
    if @current_user.present?
      redirect_to_user_home          
    else
      redirect_to_front
    end
  end

end
Houen
  • 1,039
  • 1
  • 16
  • 35