I have stumbled upon a situation where my application looks for an id that does not exist in the database. An exception is thrown. Of course, this is a pretty standard situation for any web developer.
Thanks to this answer I know that using rescue deals with the situation pretty neatly, like so:
def show
#customer = Customer.find(params[:id])
rescue ActiveRecord::RecordNotFound #customer with that id cannot be found
redirect_to action: :index #redirect to index page takes place instead of crashing
end
In case the customer cannot be found, the user gets redirected to the index page. This works absolutely fine.
Now, this is all nice, but I need to do the same rescue attempts in actions like show, edit, destroy, etc, i.e. every controller method that needs a specific id.
Having said that, here's my question:
Isn't there any way to generally tell my controller that if it can't find the id in any of its methods, it shall redirect to the index page (or, generally, perform a specific task)?
You must use rescue_from for this task. See example in the Action Controller Overview Guide
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
private
def record_not_found
redirect_to action: :index
end
end
Rails has a built-in rescue_from class method:
class CustomersController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :index
...
end
If you're talking about doing this within a single controller (as opposed to doing this globally in every controller) then here are a couple options:
You can use a before_filter to setup your resource:
class CustomerController < ApplicationController
before_filter :get_customer, :only => [ :show, :update, :delete ]
def show
end
private
def get_customer
#customer = ActiveRecord.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to :action => :index
end
end
Or you might use a method instead. I've been moving in this direction rather than using instance variables inside views, and it would also help you solve your problem:
class CustomerController < ApplicationController
def show
# Uses customer instead of #customer
end
private
def customer
#customer ||= Customer.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to :action => :index
end
helper_method :customer
end
In certain cases, I would recommend that you use Model.find_by_id(id) as opposed to Model.find(id). Instead of throwing an exception, .find_by_id returns nil. if the record could not be found.
Just make sure to check for nils to avoid NoMethodError!
P.S. For what it's worth, Model.find_by_id(id) is functionally equivalent to Model.where(id: id), which would allow you to build out some additional relations if you want.
Related
I'm working on my first rails api server.
I've got a controller for my User model that looks as such:
class UsersController < ApplicationController
def index
if current_user.admin?
#users = User.all
render json: #users
else
render json: { message: 'You do not have the appropriate permissions to access this resource' }, status: 401
end
end
def show
if User.exists?(#id)
#id = params[:id]
if current_user.id.to_s == #id || current_user.admin?
#user = User.find(#id)
render json: #user
else
render json: { message: 'You do not have the appropriate permissions to access this resource' }, status: 401
end
else
render json: { message: 'Requested resource not found' }, status: 404
end
end
end
What I want and currently have for these two controller methods is:
/users fetch all users only if the authenticated user making the request is of role admin
/users/:id fetch a user by id only if the authenticated user making the request has a matching id or is of role admin
The current implementation breaks the DRY philosophy. The reasoning is that the logic for handling whether or not the requesting user has the permissions to access the requested resource(s) is repeated across both controller methods. Furthermore, any model's controller method for show will repeat the logic for checking whether or not the requested resource exists. I also feel like this kind of implementation makes for fat controllers, where I'd rather them be skinny.
What I want to know from the community and from those that have solved this problem before; what is the best way to go about this in order to conform to the DRY philosophy and to keep controllers skinny.
Good to know: I'm using devise and devise-token-auth for authentication.
You need to use some kind of Authorization gem like cancancan. It is exactly what you need. Also it's else not elsif. elsif is followed by condition.
You can use github.com/varvet/pundit instead, for authorization.
It matches with the controller, instead of putting the authorization in the controller, you can use this to move out the authorization to another class.
I have used this across multiple Rails/Rails-API projects and didn't encounter a problem so far.
Instead of writing the code above. You can do this instead.
Also, prioritize early returns over nested ifs for readability.
In your controller.
class UsersController < ApplicationController
def index
authorize User # This will call the policy that matches this controller since this is UsersController it will call `UserPolicy`
#users = User.all
render :json => #users
end
def show
#user = User.find_by :id => params[:id] # Instead of using exists which query the data from db then finding it again, you can use find_by which will return nil if no records found.
if #user.blank?
return render :json => {:message => 'User not found.'}, :status => 404
end
authorize #user # This will call the policy that matches this controller since this is UsersController it will call `UserPolicy`
render :json => #user
end
end
In your Policy
class UserPolicy < ApplicationPolicy
def index?
#user.admin? # The policy is called in controller then this will check if the user is admin if not it will raise Pundit::NotAuthorizedError
end
def show?
#user.admin? || #record == #user # The policy is called in controller then this will check if the user is admin or the user is the same as the record he is accessing if not it will raise Pundit::NotAuthorizedError
end
end
In your ApplicationController
class ApplicationController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, :with => :show_forbidden
private
def show_forbidden exception
return render :json => {
:message => 'You are not authorized to perform this action.'
}, :status => 403
end
end
What is the best way to handle the error then ID is not found?
I have this code in my controller:
def show
#match = Match.find(params[:id])
end
I was thinking about something like this:
def show
if #match = Match.find(params[:id])
else
render 'error'
end
end
But I still get:
ActiveRecord::RecordNotFound in MatchesController#show
Couldn't findMatch with 'id'=2
Why?
What is the correct solution?
Rescue it in the base controller and leave your action code as simple as possible.
You don't want to deal not found exception in every action, do you?
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :render_404
def render_404
render :template => "errors/error_404", :status => 404
end
end
By default the find method raises an ActiveRecord::RecordNotFound exception. The correct way of handling a not found record is:
def show
#match = Match.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render 'error'
end
However, if you prefer an if/else approach, you can use the find_by_id method that will return nil:
def show
#match = Match.find_by_id(params[:id])
if #match.nil? # or unless #match
render 'error'
end
end
You can use find_by_id method it returns nil instead of throwing exception
Model.find_by_id
There is two approaches missing:
One is to use a Null-Object (there I leave research up to you)
Te other one was mentioned, but can be placed more reusable and in a way more elegantly (but it is a bit hidden from you action code because it
works on a somewhat higher level and hides stuff):
class MyScope::MatchController < ApplicationController
before_action :set_match, only: [:show]
def show
# will only render if params[:id] is there and resolves
# to a match that will then be available in #match.
end
private
def set_match
#match = Match.find_by(id: params[:id])
if !#match.present?
# Handle somehow, i.e. with a redirect
redirect_to :back, alert: t('.match_not_found')
end
end
end
I've seen many posts about rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found, along with the companion method:
def record_not_found
redirect_to action: :show
end
This is my first time implementing this, so I'm wondering why I keep getting This webpage has a redirect loop or that my params are incorrect for that route (within the record_not_found method).
When a guest user time reaches a preset timeout limit, the user record will have the #destroy method called on it--even if the guest is still within the app. The guest user has not yet inputted email/password, and the guest column in the users table is set to true. (I actually want a guest to be able to Register w/ email, etc from within the app, transferring whatever data/dependencies the guest user object had acquired to the newly created permanent user.)
I've been putting them in my individual controllers, which I know is not DRY...I'd tried this in ApplicationController and kept getting the This webpage has a redirect loop, but I'm also now getting it in my individual controllers, so I guess that wasn't the problem.
Here is an example from my lessons_controller.rb...
class LessonsController < ApplicationController
before_action :require_user, :only => [:show]
rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
def show
#lesson = Lesson.find(params[:id])
#assessment = #lesson.provide_assessment_object
if #assessment
#assessment.make_sure_choices_are_instantiated(current_user)
end
end
private
def record_not_found
redirect_to controller: :lessons, action: :show, id: params[:id]
end
end
My great preference would be to have all the record_not_found methods (hopefully eventually all DRYed up within ApplicationController) to redirect_to the front page which doesn't require a user session.
Any help on this would be most appreciate! Thanks a lot!
I have defined my own method authorize_user in one of my controllers, as:
def authorize_user
if !((current_user.has_role? :admin, #operator) || (current_user.has_role? :super_admin))
raise CanCan::AccessDenied
end
end
I want to rescue from the CanCan exception (or any other exception for that matter). I have used Rolify in my app. How do I rescue and redirect to the root_url of my app with a custom message?
I have tried the following options, but none of them worked:
Try 1:
rescue CanCan::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
end
Error in this case: syntax error, unexpected keyword_do, expecting '('
Try 2:
rescue CanCan::AccessDenied
redirect_to root_url, :alert => "Unauthorized Access"
Error in this case: Render and/or redirect were called multiple times in this action
How do I solve this issue?
This is my controller code:
class CabsController < ApplicationController
before_action :set_cab, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!
after_action :authorize_user
# Some basic CRUD actions
private
def set_cab
#cab = Cab.find(params[:id])
#operator = Operator.find(params[:operator_id])
end
def cab_params
params.require(:cab).permit(:category, :number)
end
def authorize_user
if !((current_user.has_role? :admin, #operator) || (current_user.has_role? :super_admin))
raise CanCan::AccessDenied
end
end
end
I think you could try the rescue_from method.
For example, your ApplicationController, would look like this:
class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied, with: :not_authorized
#other stuff
private
def not_authorized
redirect_to root_url, alert: "Unauthorized Access"
end
end
Since the question was updated with more code, here is additional information:
Some suggestions:
Make the :authorize_user a before_action as well. That way you don't need to worry about code running in the action even when the user was not allowed to do stuff.
You might also need to add the same :only option as for the :set_cab since you use the #operator instance variable.
Last, a personal code style preference is that I would have changed the if ! to unless to increase reading flow.
Try redirect_to(...) and return.
Agreeing with Jakob W I would like to point, that authorization (and authentication) MUST be performed only before action. What is the purpose of any authorization and exception raising when DB transaction, reading/writing to filesystem etc have been already done?
And using before_action has no problem with Render and/or redirect were called multiple times in this action - there will be only one redirect - in exception handling before controller method call.
So, I recommend next code (updated Jakob W's sample):
class CabsController < ApplicationController
#...
before_action :authorize_user
private
#...
def authorize_user
if !((current_user.has_role? :admin, #operator) || (current_user.has_role? :super_admin))
raise CanCan::AccessDenied
end
end
end
class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied, with: :not_authorized
#other stuff
private
def not_authorized
redirect_to(request.referrer || root_path), alert: "Unauthorized Access"
end
end
Could I recommend another authorization gem? I think this one is flexible and easy to use - pundit (https://github.com/elabs/pundit). Page on github has some useful tips on authorization.
In my index action, I have the following code:
#hotels = Hotel.where(lang: request.headers['Accept-Language']).includes(:contacts)
raise ActiveRecord::RecordNotFound if #hotels.blank?
I am raising the exception because I want it to be handled by an error handling code (based on rescue_from)
Is there a better way to write the code so that it does the same thing, i.e. raise the exception? I can do first! (notice the bang) when retrieving a single record, but as for collections, it seems like there is no way to do the same thing (no where!, all! ...)
Does it make sense at all?
In your controller you can add before_filter
before_filter :check_hotels, :only => [:index]
def index
end
private
def check_hotels
#hotels = Hotel.where(lang: request.headers['Accept-Language']).includes(:contacts)
redirect_to root_path, :notice => "No hotels present." if #hotels.blank?
end
Ofcourse you can give any path othet than root_path, its just an example I have shown
class HotelsController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :blank_list
private
def blank_list
logger.error "No Hotel Found With #{request.headers['Accept-Language']}"
redirect_to root_path, notice: 'No hotels present'
end
end