ruby-on-railspunditrescuerocketpants

How can I re-raise a Ruby exception in a Rails rescue_from statement?


My Rails 4 app uses RocketPants for its JSON API and Pundit for authorization.

I have code in my /app/controllers/api/v1/base_controller.rb file to handle errors from Pundit. Whenever a user isn't authorized to update a resource, Pundit throws a NotAuthorizedError exception and I rescue it with my user_not_authorized method:

class API::V1::BaseController < RocketPants::Base
  include Pundit
  version 1

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  def user_not_authorized
    error! :forbidden
  end

end

When I call the error! method that RocketPants provides from my exception handler, I expect to get a JSON response like this:

{
  "error":             "forbidden",
  "error_description": "The requested action was forbidden."
}

Instead, however, calling error! just immediately blows up the request:

Completed 500 Internal Server Error in 143ms

RocketPants::Forbidden - RocketPants::Forbidden:
  rocket_pants (1.13.1) lib/rocket_pants/controller/error_handling.rb:44:in `error!'
  app/controllers/api/v1/base_controller.rb:61:in `user_not_authorized'

Full stack trace here.

Why isn't the error! method doing what it should when called from my Pundit exception handler?

If I put error! :forbidden in the middle of my controller action, it works as expected.

For context, the controller that inherits from base_controller.rb and calls Pundit's authorize method looks like this:

class API::V1::MealsController < API::V1::BaseController

  before_filter :find_entity

  def create
    meal = @entity.meals.build(meal_params)

    authorize(@entity, :update?)

    if meal.save
      expose meal, status: :created
    else
      expose meal.errors, status: 422
    end
  end

end

Solution

  • Apparently raising exceptions in a rescue_from is a bad idea and according to the Rails docs, exceptions raised in a handler are not bubbled up:

    Exceptions raised inside exception handlers are not propagated up.

    Docs: http://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html

    Instead of re-raising RocketPants' exception, I'm simply creating and returning the JSON error message myself:

      def user_not_authorized
        # error! :forbidden
        head 403
        error = { error: 'Action not allowed.', error_description: 'Sorry, you are not allowed to perform this action.'}
        expose error
      end
    

    This works!

    UPDATE

    I've since found an even cleaner solution: just map the Pundit exception to the RocketPants exception. This means that whenever a Pundit::NotAuthorizedError error is raised it'll be treated as a RocketPants::Forbidden error.

    Got the entire solution down to a single line of code at the top of base_controller.rb:

      map_error! Pundit::NotAuthorizedError, RocketPants::Forbidden
    

    No handler required.