I am having a normal HTML frontend and a JSON API in my Rails App. Now, if someone calls /api/not_existent_method.json
it returns the default HTML 404 page. Is there any way to change this to something like {"error": "not_found"}
while leaving the original 404 page for the HTML frontend intact?

- 29,609
- 19
- 89
- 128
4 Answers
A friend pointed me towards a elegant solution that does not only handle 404 but also 500 errors. In fact, it handles every error. The key is, that every error generates an exception that propagates upwards through the stack of rack middlewares until it is handled by one of them. If you are interested in learning more, you can watch this excellent screencast. Rails has it own handlers for exceptions, but you can override them by the less documented exceptions_app
config option. Now, you can write your own middleware or you can route the error back into rails, like this:
# In your config/application.rb
config.exceptions_app = self.routes
Then you just have to match these routes in your config/routes.rb
:
get "/404" => "errors#not_found"
get "/500" => "errors#exception"
And then you just create a controller for handling this.
class ErrorsController < ActionController::Base
def not_found
if env["REQUEST_PATH"] =~ /^\/api/
render :json => {:error => "not-found"}.to_json, :status => 404
else
render :text => "404 Not found", :status => 404 # You can render your own template here
end
end
def exception
if env["REQUEST_PATH"] =~ /^\/api/
render :json => {:error => "internal-server-error"}.to_json, :status => 500
else
render :text => "500 Internal Server Error", :status => 500 # You can render your own template here
end
end
end
One last thing to add: In the development environment, rails usally does not render the 404 or 500 pages but prints a backtrace instead. If you want to see your ErrorsController
in action in development mode, then disable the backtrace stuff in your config/enviroments/development.rb
file.
config.consider_all_requests_local = false
-
4Also, don't forget to add the status code to the renders. Otherwise your client/browser will not know that it was a 404/500. render :text => "404 not found", :status => :not_found – or9ob Aug 03 '12 at 04:46
-
1I would say a respond_to block is more universal in the rendering functions: respond_to do |format| format.json { render json: {error: "not-found"}.to_json, status: 404 } format.html { render text: "404 Not found", status: 404 } end – Greg Funtusov Apr 19 '13 at 16:11
-
1I've namespaced my API controllers and throw an exception that has a base class like ApiException which can then be rescued in the base namespaced Api controller, where a JSON friendly error can be returned with the appropriate status, like above. – RidingTheRails May 03 '13 at 11:20
-
@RichardHollis My API controllers are namespaced as well but would I achieve what you did? – darksky Jul 17 '13 at 16:25
-
@Darksky not sure what you mean? – RidingTheRails Jul 18 '13 at 12:43
-
@RichardHollis by namespaced did you mean you had them all in a module? If so, how did you create a base class ApiException that handles all errors (including 404 and 500)? – darksky Jul 18 '13 at 12:49
-
@Darksky see this Gist - https://gist.github.com/richhollis/6029217 - I mean namespaced in the directory sense – RidingTheRails Jul 18 '13 at 13:18
-
@RichardHollis Have you ever used rspec to test controllers with a subdomain constraint? – darksky Jul 18 '13 at 13:23
-
@Darksky no, but funnily enough I did look into this recently because I project I was working on might have needed sub domains - I found this article which looked promising: http://stackoverflow.com/questions/2556627/rails-rspec-set-subdomain – RidingTheRails Jul 19 '13 at 00:56
-
Rails 4 will give you `You should not use the `match` method in your router without specifying an HTTP method` errors with the routes file. – Sarah Vessels Aug 26 '13 at 15:19
-
@iblue How to I handle test for this provided answer – Afolabi Olaoluwa Nov 05 '16 at 23:14
-
3Check out this answer before resorting to the solution proposed above: http://stackoverflow.com/a/29292738/2859525. Its much simpler to just catch 404 in an ancestor controller and use a callback to make a simple JSON error response. – Todd Apr 09 '17 at 00:42
I like to create a separate API controller that sets the format (json) and api-specific methods:
class ApiController < ApplicationController
respond_to :json
rescue_from ActiveRecord::RecordNotFound, with: :not_found
# Use Mongoid::Errors::DocumentNotFound with mongoid
def not_found
respond_with '{"error": "not_found"}', status: :not_found
end
end
RSpec test:
it 'should return 404' do
get "/api/route/specific/to/your/app/", format: :json
expect(response.status).to eq(404)
end

- 1,377
- 15
- 18
-
4This seems to work for records only. How would you manage the case for `api/non_existant_route`? – Sebastialonso Feb 22 '15 at 14:05
-
1the line `rescue_from ActiveRecord::RecordNotFound, with: not_found` needs to be `with: :not_found` but it's only a one character edit :P – erroric May 08 '15 at 20:03
Sure, it will look something like this:
class ApplicationController < ActionController::Base
rescue_from NotFoundException, :with => :not_found
...
def not_found
respond_to do |format|
format.html { render :file => File.join(Rails.root, 'public', '404.html') }
format.json { render :text => '{"error": "not_found"}' }
end
end
end
NotFoundException
is not the real name of the exception. It will vary with the Rails version and the exact behavior you want. Pretty easy to find with a Google search.

- 5,283
- 1
- 24
- 30
-
2Thanks for the idea, but I am using Rails 3.2.2 where the exception handling changed. This will no longer work. – iblue Apr 20 '12 at 21:38
-
@iblue This works just fine in Rails 3.2+ and is the better solution. See the `rescue_from` [docs](http://apidock.com/rails/v3.2.3/ActiveSupport/Rescuable/ClassMethods/rescue_from) for more info. – Intelekshual Aug 07 '12 at 17:42
Try to put at the end of your routes.rb
:
match '*foo', :format => true, :constraints => {:format => :json}, :to => lambda {|env| [404, {}, ['{"error": "not_found"}']] }

- 15,665
- 2
- 46
- 48
-
1For anyone else reading this, `match` in the `Rails.application.routes` file now requires you to specify the HTTP method, through the `via:` parameter. This will also show up as an error when you attempt to use the above line. – sameers Jul 22 '17 at 18:32