Search code examples
ruby-on-railsrubyerror-handlingapi-design

Best way to perform error handling using modules in Rails?


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.


Solution

  • 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.