I'm pretty new to Rails and back-end API developement so excuse me if I misuse a concept or so. Right now I'm attempting to refactor a large amount of conditional error handling code that is sprinkled around the code base and move towards using an explicit list of rescued exceptions that's mixed into the API controller by including it in as a module. This will allow me to attach custom, but arbitrary, codes to each exception caught, so long as we use the bang alternatives for active record methods, and the error handling code can live in one place. So far, for the error handling module, I have something like this:
# app/lib/error/error_handling.rb
module Error
module ErrorHandling
def self.included(klass)
klass.class_eval do
rescue_from ActiveRecord::RecordNotFound do |e|
respond(:record_not_found, 404, e.to_s)
end
rescue_from ActiveRecord::ActiveRecordError do |e|
respond(e.error, 422, e.to_s)
end
rescue_from ActiveController::ParameterMissing do |e|
response(:unprocessable_entitry, 422, e.to_s)
end
rescue_from ActiveModel::ValidationError do |e|
response(e.error, 422, e.to_s)
end
rescue_from CustomApiError do |e|
respond(e.error, e.status, e.message.to_s)
end
rescue_from CanCan::AccessDenied do
respond(:forbidden, 401, "current user isn't authorized for that")
end
rescue_from StandardError do |e|
respond(:standard_error, 500, e.to_s)
end
end
end
private
def respond(_error, _status, _message)
render "layouts/api/errors", status: _status
end
end
end
Where layouts/api/errors is a view built using jbuilder.
In the ApiController we have:
# app/controllers/api/api_controller.rb
module Api
class ApiController < ApplicationController
include Error::ErrorHandling
attr_reader :active_user
layout "api/application"
before_action :authenticate_by_token!
before_action :set_uuid_header
respond_to :json
protect_from_forgery with: :null_session
skip_before_action :verify_authenticity_token, if: :json_request?
private
...
end
Unfortunately this doesn't seem to work. Running tests shows that the private methods are not being loaded at all and are considered undefined!
To be more specific, here are the errors emitted:
uninitialized constant Error::ErrorHandling::ActiveController
and
undefined local variable or method `active_user' for Api::FooController
Where active_user is an attribute that is set inside of an instance variable by a method named set_active_user. Which is obviously not being called.
However the ErrorHandling module is being evaluated. How could this be? Am I namespacing incorrectly or something?
Thanks for reading.
The answer is broken down into two parts as I believe that there are two separate problems.
unitinalized constant error
The error
uninitialized constant Error::ErrorHandling::ActiveController
can be fixed by changing this
rescue_from ActiveController::ParameterMissing do |e|
response(:unprocessable_entitry, 422, e.to_s)
end
to this:
rescue_from ::ActiveController::ParameterMissing do |e|
response(:unprocessable_entitry, 422, e.to_s)
end
(adding :: in front of the ActiveController constant)
Constant lookup in ruby takes lexical nesting into account. As you reference the Constant within
module Error
module ErrorHandling
...
end
end
ruby will try to find the constant within this namespace if the constant is undefined before. Prepending :: will tell ruby to ignore the nesting on constant lookup.
undefined local method
The error
undefined local variable or method `active_user' for Api::FooController
is raised because some code is calling the instance method active_user on the class Api::FooController where it is not defined.
I know this is an old question but I was struggling with it but found the fix. To fix the issue of:
`undefined local variable or method `active_user' for Api::FooController`
You need to include extend ActiveSupport::Concern e.g.
module Error
module ErrorHandling
extend ActiveSupport::Concern
# rest of your code
end
end
Related
I want to use rescue_from method on a model concern but neither exception error is rescued to the method i specified:
require "active_support/concern"
module CustomErrorHandler
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
included do
rescue_from StandardError, with: :capture_error
end
private
def capture_error(e)
puts "Error catched!"
end
end
class FooClass
include CustomErrorHandler
def some_action
raise StandardError, 'Error'
end
end
err = FooClass.new
err.some_action
Traceback (most recent call last):
2: from (irb):28
1: from (irb):23:in `some_action'
StandardError (Error) # Didn't catch the error!
Anyone can help me on how can i solve this? Thanks!
This idea is fundamentially broken.
rescue_from is basically just syntactic sugar for:
class ThingsController < ApplicationController
around_action :wrap_in_a_rescue
def show
raise SomeKindOfException
end
private
def wrap_in_a_rescue
begin
yield
rescue SomeKindOfException
do_something_else
end
end
def do_something_else
render plain: 'Oh Noes!'
end
end
It only works because the controller declares a callback.
Its use makes sense in a controller as it rescues exceptions that occur in its callbacks as well as in the method itself and can be used with inheritiance to customize the error responses. It use does not make very much sense outside of that context.
While you could maybe fix this code by including ActiveSupport::Callbacks and declaring the requisite callbacks it is most likely a very overcomplicated solution to the original problem which can most likely be handled with a simple rescue or composition.
In my application_controller.rb I have the following line
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
Now, in one method, I would like to handle that specific error differently and I've added this to one method in a controller.
class MyClass < ApplicationController
def my_method
# raising the Pundit::NotAuthorizedError in the method
authorize resource, :my_method?
rescue Pundit::NotAuthorizedError
# code on how to deal with the error
end
end
If I execute the code, the error handler from application_controller.rb will be handling my error instead of the error handler in the method.
So my question is, what is the precedence of the error handlers and is there any way I can change this, so that the error is handled in the method and not globally?
Please forget my previous answer, I myself made a mistake when reproducing your issue. In deed I am not able to reproduce it.
Please have a look at this little demo app I created:
https://github.com/andi-dev/rescue-handler-demo/blob/master/app/controllers/peas_controller.rb
The rescue_from handler is not executed, and the browser shows what I render from the rescue block within the action-method.
I'm trying to access the request object in app/controllers/application_controller.rb. My code is:
class ApplicationController < ActionController::API
include ActionView::Layouts
include ActionController::RequestForgeryProtection
protect_from_forgery
before_action :require_login
private
def require_login
unless logged_in?
logger.log "#{request}"
end
end
def logged_in?
false
end
end
This results in the error:
comparison of String with 0 failed, highlighting the line logger.log "#{request}"
I thought it was a problem with the middleware not being loaded, so I tried to load it in config/application.rb:
config.middleware.use ActionDispatch::Request
But this results in another error:
undefined method 'call' for ActionDispatch::Request
I seem to keep having to add things back in since I used the --api option and it strips a lot of things out. But I don't know how to add back in access to the request option. Any help?
It looks like you are mis-using logger.log. In the simple example below, I have outlined three ways to approach this. If you want to use logger.log, you need to specify at minimum a severity level. That is the source of the comparison of String with 0 failed message you are receiving.
class ApplicationController < ActionController::API
before_action :log_request
def log_request
logger.log request # This doesn't work
logger.info("#{request}") # This works
logger.log(Logger::WARN,"#{request}") # This works
end
end
Valid levels are: FATAL, ERROR, WARN, INFO, DEBUG.
Reference: http://ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html#method-i-add (logger.log is an alias for logger.add)
Logger#add(severity, message = nil, progname = nil) { ... }
Rails's :rescue_from takes in a specific exception type and a method as parameter as follow:
class ApplicationController < ActionController::Base
rescue_from User::NotAuthorized, with: :deny_access # self defined exception
rescue_from ActiveRecord::RecordInvalid, with: :show_errors
rescue_from 'MyAppError::Base' do |exception|
render xml: exception, status: 500
end
protected
def deny_access
...
end
def show_errors(exception)
exception.record.new_record? ? ...
end
end
but this implies that it will deal with the specified exception in the same way ALL ACROSS the controller.
What if I want to handle an exception type differently based on what method the exception is raised from, Example:
class MyController < ActionController::Base
def method_1
# Do Something
rescue MyCustomError => e
handle_exception_for_method_1(e)
end
def method_2
# Do Something
rescue MyCustomError => e
handle_exception_for_method2(e)
end
protected
def handle_exception_for_method_1(exception)
# Do Something
end
def handle_exception_for_method_2(exception)
# Do Something
end
end
I have the following questions:
Can this be done by using :rescue_from as well (with any sort of options to pass in)?
If not, is there any better solution of dealing with this kind of situations?
(Kind of off topic but) Is it a bad practice to handle the same type of error differently in different methods in general?
Rails provides access to the controller and action names through controller_name and action_name methods. You could use this to handle exceptions differently based on the what method the exception was raised.
Example:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordInvalid, with: :show_errors
protected
def show_errors
if action_name == "create"
...
elsif action_name == ...
...
end
end
I tried almost everything on web, all I want is to call a method whenever an exception like "ActiveRecord::RecordNotFound" or "No route matches" appears.
Rescues from ApplicationController does not work, but why?
class ApplicationController < ActionController::Base
protect_from_forgery
private
def self.send_report_error(message)
Notifier.page_failure(message).deliver
end
rescue ActiveRecord::RecordNotFound
# handle not found error
send_report_error ActiveRecord::RecordNotFound.to_s
rescue ActiveRecord::ActiveRecordError
# handle other ActiveRecord errors
send_report_error ActiveRecord::ActiveRecordError.to_s
rescue # StandardError
# handle most other errors
send_report_error "common error"
rescue Exception
# handle everything else
send_report_error "common exception"
end
Use rescue_from. For example:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :send_report_error
end
http://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html