AbstractController::DoubleRenderError with controller exception handling - ruby-on-rails

In my Rails 3.1 app, I have a rescue_from on top of the base API controller that looks like this:
rescue_from Exceptions::InvalidApiKey, :with => :invalid_api_key
in the invalid_api_key looks like this:
def invalid_api_key
Rails.logger.debug "Invalid API key"
render :json => {:error => :invalid_api_key}, :status => :forbidden
end
There is a before_filter that checks the API key and throws an InvalidApiKey exception if needed.
However, I get an AbstractController::DoubleRenderError when I try the controller with the wrong API key, as it tries to render in the exception handler and also in the original action.
How can I avoid this?

You need to return false in before_filter to stop the controller's chain.
Otherwise, it firstly renders error in invalid_api_key and goes to called action using "usual" flow (since you've rescued from exception) and tries to render again from the action.
So
def invalid_api_key
Rails.logger.debug "Invalid API key"
render :json => {:error => :invalid_api_key}, :status => :forbidden
false
end
should solve the issue

Related

Where vs Find in Rails Api

Let's say I have a student Rails API which having an endpoint that looks like http://www.example.com/students/1
What is the preferred way to implement?
review = Review.find(inputs[:review_id])
To handle exceptions,
rescue_from Exception, :with => :internal_error
def internal_error(e)
render json: {error: {message: "Internal Error"} }, :status => 500
end
OR
review = Review.where(inputs[:review_id]).first
if review.nil?
render json: {error: {message: "Internal Error"} }, :status => 500
end
My question is which is better way for handling non-existent id through the url.
You should go with the first approach
# reviews_controller.rb
review = Review.find(inputs[:review_id])
And
# application_controller.rb
# rescue_from Exception, :with => :internal_error
# OR Prefer ActiveRecord::RecordNotFound
rescue_from ActiveRecord::RecordNotFound, :with => :internal_error # Prefer this one
def internal_error(e)
render json: {error: {message: "Internal Error"} }, :status => 500
end
To make it generic, Add it to application_controller.rb
NOTE:
This way you don't have to rescue it in every controller (the second approach you have to)
You can add a global rescue_from in your base controller (ApplicationController for example) and then use the find method (Best way to retrieve only one record) :
rescue_from ActiveRecord::RecordNotFound do |e|
render status: :not_found, json: { error: { message: e.message } }
end
Every time you try to retrieve a record, if he doesn't exist you will render an error message and a 404 status which stand for a non-existent resource.
You should use rescue for manage error
def action_name
review = Review.find(inputs[:review_id])
render json: review, status: :ok
rescue # for ever not found
render json: {}, status: :not_found,nothing: true
end
doc for status list
and you can use rescue_from on header but this works for every action
rescue_from ActiveRecord::RecordNotFound,with: action_name
Neither. You can just do something like:
unless review = Review.find_by(id: inputs[:review_id])
render json: {error: {message: "record not found"} }, status: :not_found
end
Benefits:
Does't require endless nil checking as mentioned in comments.
Avoids unnecessary exception handling.
Returns a more informative error message.

Rails generic not found controller method

In my controller I have this code in one of my actions:
begin
#user = User.find(params[:id])
#user.destroy
rescue ActiveRecord::RecordNotFound
render :json => {"status" => "404", "message" => "User with id #{params[:id]} not found"}
return
end
And is working fine, but I dont want to copy paste it to all the methods which require to run a Select query.
So I found this answer How to redirect to a 404 in Rails?
And then tried slightly different since I am rendering JSON API endpoints instead templates.
Note also I dont know if that params[:id] will be defined there.
def not_found
render :json => {"status" => "404", "message" => "User with id #{params[:id]} not found"}
end
Anyway I modified the query with:
#user = User.find(params[:id]) or not_found
But is still raising the ActiveRecord::RecordNotFound exception.
Also would it be possible to create a generic not_found action which I can use in all the controllers which I can pass it the id parameter and the type of the Object?
Like some generic 404, 500, 400, 200 methods which render JSON responses where I can just pass some parameters
Use rescue_from in your ApplicationController:
class ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :show_not_found_errors
# All the information about the exception is in the parameter: exception
def show_not_found_errors(exception)
render json: {error: exception.message}, status: :not_found
end
end
Thus, any ActiveRecord::RecordNotFound will be rescued with show_not_found_errors method. Add these codes in ApplicationController, and it will works for all the others controllers which is inherited from ApplicationController.

log backtrace, rescue_from and custom error pages

I am trying to create custom error pages in a project of
rails 3.0.20 and ruby 1.8.7.
Anyway in my application controller:
unless Rails.application.config.consider_all_requests_local
rescue_from Exception, :with => :render_error
rescue_from ActionController::RoutingError, ActionController::UnknownController, ::AbstractController::ActionNotFound, ActiveRecord::RecordNotFound, :with => :render_error_not_found
end
Then the render error method:
def render_error(exception)
notify_airbrake(exception)
Rails.logger.fatal exception
Rails.logger.fatal exception.backtrace.join("\n")
respond_to do |format|
format.html { render :template => "errors/error_500", :layout => 'layouts/application'}
format.all { render :nothing => true, :status => 500 }
end
end
It seems that now my logs are being filled with a much longer then usual backtrace.
Why is that happening? Is there a way to show just the "important" part of a backtrace?
And is it correct to call airbrake here?
Thanks
Check out http://api.rubyonrails.org/classes/ActiveSupport/BacktraceCleaner.html. Rails uses this to clean up your backrace before display.
You could use it like this:
Rails.backtrace_cleaner.clean(exception.backtrace)
I'm actually looking in the rails source code, because I thought your exception might have been cleaned already by the time it gets to your render_error method.

Need to return JSON-formatted 404 error in Rails

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?
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
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
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.
Try to put at the end of your routes.rb:
match '*foo', :format => true, :constraints => {:format => :json}, :to => lambda {|env| [404, {}, ['{"error": "not_found"}']] }

How to redirect to a 404 in Rails?

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?
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
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
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.
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."
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.
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
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
<%= 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.
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
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:
It hooks into the existing error handling middleware like a normal ActionController::RoutingError, but you get a more meaningful error message in dev environments
It will correctly set the status to whatever you specify in the rescue_responses hash (in this case 404 - not_found)
You don't have to write a not_found method that needs to be available everywhere.
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
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

Resources