Custom Error Handling with Rails 4.0
Asked Answered
G

5

16

I'm building a Ruby on Rails api using Ruby 2.0 and Rails 4.0. My app is almost solely a JSON API, so if an error occurs (500, 404), I want to capture that error and return a nicely formatted JSON error message.

I've tried this and also:

rescue_from ActionController::RoutingError, :with => :error_render_method

def error_render_method
  puts "HANDLING ERROR"
  render :json => { :errors => "Method not found." }, :status => :not_found
  true
end

In my ApplicationController.

Neither of these do the trick (the exceptions are not captured at all). My Googling shows that this changed a lot between 3.1, 3.2, and I can't find any good documentation on how to do this in Rails 4.0.

Anybody know?

Edit Here's the stack trace when I go to a 404 page:

Started GET "/testing" for 127.0.0.1 at 2013-08-21 09:50:42 -0400

ActionController::RoutingError (No route matches [GET] "/testing"):
actionpack (4.0.0) lib/action_dispatch/middleware/debug_exceptions.rb:21:in `call'
actionpack (4.0.0) lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
railties (4.0.0) lib/rails/rack/logger.rb:38:in `call_app'
railties (4.0.0) lib/rails/rack/logger.rb:21:in `block in call'
activesupport (4.0.0) lib/active_support/tagged_logging.rb:67:in `block in tagged'
activesupport (4.0.0) lib/active_support/tagged_logging.rb:25:in `tagged'
activesupport (4.0.0) lib/active_support/tagged_logging.rb:67:in `tagged'
railties (4.0.0) lib/rails/rack/logger.rb:21:in `call'
actionpack (4.0.0) lib/action_dispatch/middleware/request_id.rb:21:in `call'
rack (1.5.2) lib/rack/methodoverride.rb:21:in `call'
rack (1.5.2) lib/rack/runtime.rb:17:in `call'
activesupport (4.0.0) lib/active_support/cache/strategy/local_cache.rb:83:in `call'
rack (1.5.2) lib/rack/lock.rb:17:in `call'
actionpack (4.0.0) lib/action_dispatch/middleware/static.rb:64:in `call'
railties (4.0.0) lib/rails/engine.rb:511:in `call'
railties (4.0.0) lib/rails/application.rb:97:in `call'
rack (1.5.2) lib/rack/lock.rb:17:in `call'
rack (1.5.2) lib/rack/content_length.rb:14:in `call'
rack (1.5.2) lib/rack/handler/webrick.rb:60:in `service'
/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/webrick/httpserver.rb:138:in `service'
/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/webrick/httpserver.rb:94:in `run'
/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/webrick/server.rb:295:in `block in start_thread'


Rendered /Library/Ruby/Gems/2.0.0/gems/actionpack-4.0.0/lib/action_dispatch/middleware/templates/rescues/_trace.erb (1.0ms)
Rendered /Library/Ruby/Gems/2.0.0/gems/actionpack-4.0.0/lib/action_dispatch/middleware/templates/routes/_route.html.erb (2.9ms)
Rendered /Library/Ruby/Gems/2.0.0/gems/actionpack-4.0.0/lib/action_dispatch/middleware/templates/routes/_route.html.erb (0.9ms)
Rendered /Library/Ruby/Gems/2.0.0/gems/actionpack-4.0.0/lib/action_dispatch/middleware/templates/routes/_table.html.erb (1.1ms)
Rendered /Library/Ruby/Gems/2.0.0/gems/actionpack-4.0.0/lib/action_dispatch/middleware/templates/rescues/routing_error.erb within rescues/layout (38.3ms)

I don't think I want it to ever get this far, something should catch it and return the appropriate json error response.

Gereron answered 21/8, 2013 at 13:42 Comment(1)
Hmmmm, this may actually be working how I've got it setup... Let me test it some more.Gereron
A
17

The request isn't even hitting your app.

You need to define a catchall route so Rails will send the request to your app rather than display an error (in development) or render the public/404.html page (in production)

Modify your routes.rb file to include the following

match "*path", to: "errors#catch_404", via: :all

And in your controller

class ErrorsController < ApplicationController

  def catch_404
    raise ActionController::RoutingError.new(params[:path])
  end
end

And your rescue_from should catch the error then.

Avril answered 11/12, 2013 at 0:47 Comment(2)
You may want to change get in routes to match, as this could happen for other request methods too.Elvieelvin
Rails 4 will remind you to "expose your action to both GET and POST, [by] add[ing] via: [:get, :post]" - remember to do that too! :)Medicable
L
16

After trying a few variations I've settle on this as the simplest way to handle the API 404s:

# Passing request spec
describe 'making a request to an unrecognised path' do
  before { host! 'api.example.com' }
    it 'returns 404' do
    get '/nowhere'
    expect(response.status).to eq(404)
  end
end

# routing
constraints subdomain: 'api' do
  namespace :api, path: '', defaults: { format: 'json' } do
    scope module: :v1, constraints: ApiConstraints.new(1) do
      # ... actual routes omitted ...
    end
    match "*path", to: -> (env) { [404, {}, ['{"error": "not_found"}']] }, via: :all
  end
end
Loyola answered 1/8, 2014 at 11:50 Comment(1)
+1 for keeping it in the routes file and adding a test. I've used this more or less verbatim in my app, thanks!Tarrsus
M
9

this works in rails4, this way you can manage directly all errors: for example you can render error_info as json when an an error occurs from an api call..

application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery


  # CUSTOM EXCEPTION HANDLING
  rescue_from StandardError do |e|
    error(e)
  end

  def routing_error
    raise ActionController::RoutingError.new(params[:path])
  end

  protected

  def error(e)
    #render :template => "#{Rails::root}/public/404.html"
    if env["ORIGINAL_FULLPATH"] =~ /^\/api/
    error_info = {
      :error => "internal-server-error",
      :exception => "#{e.class.name} : #{e.message}",
    }
    error_info[:trace] = e.backtrace[0,10] if Rails.env.development?
    render :json => error_info.to_json, :status => 500
    else
      #render :text => "500 Internal Server Error", :status => 500 # You can render your own template here
      raise e
    end
  end

  # ...

end

routes.rb

MyApp::Application.routes.draw do

  # ...

  # Any other routes are handled here (as ActionDispatch prevents RoutingError from hitting ApplicationController::rescue_action).
  match "*path", :to => "application#routing_error", :via => :all
end
Marrin answered 1/9, 2013 at 16:35 Comment(0)
K
2

I used the 404.html from public folder and this is in dev environment.
I actually got the answer from:

However, I did a little experiment on what pieces of code actually made it work. Here's are the pieces of code that I only added.

config/routes.rb

Rails.application.routes.draw do
    // other routes
    match "*path", to: "application#catch_404", via: :all
end

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
    def catch_404
        render :file => 'public/404.html', :status => :not_found
    end
end

Will appreciate any comments and clarifications as to why some of the original are are needed. For instance, using this line of code

raise ActionController::RoutingError.new(params[:path])

and this

rescue_from ActionController::RoutingError, :with => :error_render_method

Because rescue_from and raise ActionController::RoutingError seem to be the popular answer from the older Rails versions.

Knute answered 9/9, 2014 at 20:10 Comment(0)
H
0

Try this if you want respond to all types of errors in the same way

rescue_from StandardError, :with => :error_render_method

If you don't want this behavior in your development mode, add the above code under

unless Rails.application.config.consider_all_requests_local

Hula answered 21/8, 2013 at 13:48 Comment(3)
Good to know, but it looks like this isn't catching the error at all (I added a stack trace). My "error_render_method" is never being called.Gereron
I am using rails 3.2.13 and it works for me. May be they changed something around this in rails 4Hula
This would not be called in few different cases if you included it in your ActionController wrong. From the docs: "The handler of the first class for which exception.is_a?(klass) holds true is the one invoked, if any"K

© 2022 - 2024 — McMap. All rights reserved.