Rails 4; Raise an error and catch within controller - ruby-on-rails

I'd love to define a rescue_from handler in my controller to respond to the error.
modudle Api
module V1
class TreesController < Api::V1::ApiController
rescue_from TreeNotFound, with: :missing_tree
def show
#tree = find_tree
end
private
def missing_tree(error)
redirect_to(action: :index, flash: error.message)
end
def find_tree
find_forest.trees.find(params[:id])
rescue ActiveRecord::RecordNotFound
raise TreeNotFound, "Couldn't find a tree to hug"
end
end
end
end
However I got some error Api::V1::TreesController::TreeNotFound.
Any idea?
Update
# api_controller.rb
module Api
module V1
class ApiController < JSONAPI::ResourceController
skip_before_action :verify_authenticity_token # Disable CSRF to enable to function as API
respond_to :json
# NOTE: This block is used when you put unrelated values
rescue_from(ArgumentError) do |e|
render json: { error: e.message }, states: 400 # :bad_request
end
rescue_from(ActionController::ParameterMissing) do |e|
error = {}
error[e.param] = ['parameter is required']
response = { errors: [error] }
render json: response, status: 422 # :unprocessable_entity
end
end
end
end

you need to declare the error class first before you can use it. Do this by inheriting from StandardError.
class TreeNotFound < StandardError
end

Related

Rails api how to handle service object exceptions in controller

I wanted use Service Object in my Rails API. Inside my Service Object I want to save Model and return true if saving was successful, but if saving was unsuccessful then I want to return false and send error messages. My Service Object looks like that:
class ModelSaver
include ActiveModel::Validations
def initialize(params)
#params = params
end
def save_model
model ||= Model.new(params)
return false unless model.valid?
model.save!
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
model.errors.add(:base, 'here I want default error message')
false
end
private
attr_reader :params
end
The problem is that I don't know how to send errors messages in response. When I try to send service_object.errors.messages it displays an empty array to me. It looks like that in Controller:
class ModelController < ApplicationController
...
def create
service_object = ModelSaver.new(params)
if service_object.save_model
render json: service_object
else
render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
end
end
...
end
So how can I get Model errors from Service Object inside Controller?
You can solve this by providing methods to your service object that allow returning the model or the errors even after save_model returned with false.
I would change the service object to
class ModelSaver
include ActiveModel::Validations
attr_reader :model
def initialize(params)
#params = params
end
def errors
#model.errors
end
def save_model
#model ||= Model.new(params)
return false unless model.valid?
#model.save!
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
#model.errors.add(:base, 'here I want default error message')
false
end
private
attr_reader :params
end
and the controller method to
def create
service_object = ModelSaver.new(params)
if service_object.save_model
render json: service_object.model
else
render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
end
end
I would refactor the service object so that it just delegates to the underlying model:
class ModelSaver
attr_reader :model
delegate :errors, to: :model
def initialize(**params)
#model = Model.new(**params)
end
def save_model
if model.save
true
else
model.errors.add(:base, 'you forgot to frobnobize the whatchamacallit')
false
end
end
end
class ModelController < ApplicationController
def create
service_object = ModelSaver.new(**params)
if service_object.save_model
render json: service_object
else
render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
end
end
end

How can I render a custom JSON error response "Couldn't find Plan with id" in Rails?

I would like to render a custom error that shows when the user cannot find the class 'Plan' by id. The problem is that it's not reaching the if statement.
NB. I am using Insomnia to test it.
class Api::V1::PlansController < Api::V1::BaseController
before_action :authorize_request
def show
#plan = Plan.find(params[:id])
if #plan.errors.any?
render json 'wrong'
else
#accounts = #plan.accounts
render json: #plan, status: :created
end
end
end
ActiveRecord::FinderMethods#find will raise an ActiveRecord::RecordNotFound exception when if one the ids cannot be found. And when an exception is raised it halts execution.
You can handle the exception by using rescue_from:
# Do not use the scope resolution operator when declaring classes and modules
# #see https://github.com/rubocop-hq/ruby-style-guide#namespace-definition
module Api
module V1
class PlansController < BaseController
before_action :authorize_request
rescue_from ActiveRecord::RecordNotFound, with: :not_found
def show
#plan = Plan.find(params[:id])
render json: #plan
end
private
def not_found
render json: { error: 'not found' }, status: :not_found
end
end
end
end
The recommendation to use find_by might sound like a good idea initially until you realize that the exception is really useful as it halts execution of the action and prevents nil errors.
module Api
module V1
class PlansController < BaseController
# ...
before_action :set_plan
def update
# this would create a nil error if we had used 'find_by'
if #plan.update(plan_params)
# ...
else
# ...
end
end
private
def set_plan
#plan = Plan.find(params[:id])
end
end
end
end
Using rescue_from is also a really powerful pattern as it lets you move error handling up in the inheritance chain instead of repeating yourself:
module Api
module V1
class BaseController < ::ActionController::Api
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def not_found
render json: { error: 'not found' }, status: :not_found
end
end
end
end
But most likely you don't even need this at all in the first place. Rails rescues ActiveRecord::RecordNotFound on the framework level by sending a 404 - Not Found response. Clients should not need any more context then the status code in this case and returning completely unessicary JSON error message responses is an anti-pattern.

AbstractController::DoubleRenderError during API call

I'm trying to set up an API that logs in a user to my application and authenticates the user with a token. For this I am creating a new session.
Sessions controller
class Api::V1::SessionsController < Api::V1::BaseController
def create
user = User.where(email: params[:email]).first
if user&.valid_password?(params[:password])
render json: user.as_json(only: [:first_name, :last_name, :email]), status: :created
else
head(:unauthorized)
end
end
Base controller
class Api::V1::BaseController < ActionController::API
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
rescue_from StandardError, with: :internal_server_error
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def internal_server_error(exception)
if Rails.env.development?
response = { type: exception.class.to_s, message: exception.message, backtrace: exception.backtrace }
else
response = { error: "Internal Server Error" }
end
render json: response, status: :internal_server_error
end
def user_not_authorized(exception)
render json: {
error: "Unauthorized #{exception.policy.class.to_s.underscore.camelize}.#{exception.query}"
}, status: :unauthorized
end
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
end
When I test the method, I receive AbstractController::DoubleRenderError, despite the user existing in the database. It points to the last line of the internal_server_error(exception) method of the BaseController.
Can someone help me understand why this code doesn't render the JSON of the user, when the user actually does exist in the database?

how to handle ActiveRecord::RecordNotFound in both ActionController::API and ActionController::Base

In this post, the errors are rescued in the both api and base controller methods. But it might not be best approach to handle errors because of some reasons are:
Fat Controllers
DRY
Maintainability
In ActionController::Base, we handled ActiveRecord::RecordNotFound in only ApplicationController. But for ActionController::API i have to rescue ActiveRecord::RecordNotFound in every controller. So are there any best approach for handle this problem?
Using Rails 5 and 'active_model_serializers' gem for api
ActionController::API
module Api
module V1
class UsersController < ActionController::API
before_action :find_user, only: :show
def find_user
#user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render json: { error: e.to_s }, status: :not_found
end
end
end
end
ActionController::Base
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found
render file: "#{Rails.root}/public/404", layout: true, status: :not_found
end
end
You can do something like this in application_controller.rb
if Rails.env.production?
rescue_from ActiveRecord::RecordNotFound, with: :render_404
end
def render_404
render json: {meta: meta_response(404, "Record not found")}
end
This would rescue all RecordNotFound exception with 404 but only in production mode.
ActionController::API includes the ActionController::Rescue module which is what provides the rescue_from class method.
I would create an Api::BaseController base class that the Api::V1::UsersController can use instead of using ActionController::API on each controller class. This would allow you have a rescue_from in a single place instead of needing a rescue block on every action.
module Api
class BaseController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
render json: { error: e.to_s }, status: :bad_request
end
end
module V1
class UsersController < BaseController
def find_user
#user = User.find(params[:id])
end
end
end
end
I'd also further create an Api::V1::BaseController to allow for easier versioning of the APIs. Then, if you decide to change the format of the errors for v2, just move the rescue_from in the Api::BaseController to the Api::V1::BaseController, and add a new rescue_from to the new Api::V2::BaseController.
module Api
class CommonBaseController < ActionController::API
# code common across API versions
end
module V1
class BaseController < CommonBaseController
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
render json: { error: e.to_s }, status: :bad_request
end
end
end
module V2
class BaseController < CommonBaseController
# use a custom base error class to make catching errors easier and more standardized
rescue_from BaseError, with: :handle_error
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
status, status_code, code, title, detail =
if e.is_a?(ActiveRecord::RecordNotFound)
[:not_found, '404', '104', 'record not found', 'record not found']
else
[
e.respond_to?(:status) ? e.status : :bad_request,
e.respond_to?(:status_code) ? e.status_code.to_s : '400',
e.respond_to?(:code) ? e.code.to_s : '100',
e.respond_to?(:title) ? e.title : e.to_s,
e.respond_to?(:detail) ? e.detail : e.to_s
]
end
render(
json: {
status: status_code,
code: code,
title: title,
detail: detail
},
status: status
)
end
end
end
end

Active Admin custom 400, 500 error pages

How would you implement custom 400 and 500 error pages in active admin? The pages must use the active admin layout.
into your routes:
match "/404", to: "errors#render_error"
match "/500", to: "errors#render_error"
build new controller called ErrorsController
class ErrorsController < ApplicationController
def render_error
exception = env["action_dispatch.exception"]
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
#method, #message = if status_code == 404
["not_found", env["REQUEST_URI"]]
else
["server_error", "#{exception.message}\n#{exception.backtrace.join('\n')}"]
end
render status: status_code
end
end
then in your ApplicationController:
private
def not_found
raise ActionController::RoutingError.new('Not Found')
end
then create new view to show any html you want.
hope it helps you.

Resources