I have an application in rails that can respond both to json and html.
I'd like that, when a record returns an error while saving, the json answer is like that:
{
"errors" : {
"slug" : [
"can't be blank"
],
"title" : [
"can't be blank"
]
}
}
So I added this code to my ApplicationController class.
rescue_from ActiveRecord::RecordInvalid do |exception|
render json: { errors: exception.record.errors },
status: :unprocessable_entity
end
I'd like that this rescue_from is called only when the format is json, otherwise behave in the standard way (when the format is html). How can I do something like this?
UPDATE
I found a solution, but I don't think that it's very good:
rescue_from ActiveRecord::RecordInvalid do |exception|
respond_to do |format|
format.json do
render json: { errors: exception.record.errors },
status: :unprocessable_entity
end
format.html { fail exception }
end
end
Is there a better approach?
There is a better approach.
For example:
rescue_from ActionController::InvalidAuthenticityToken do |exception|
raise unless request.xhr?
render nothing: true, status: :unprocessable_entity
end
Related
Currently i am catching the error not_found like this
def show
begin
#task = Task.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render json: { error: e.to_s }, status: :not_found and return
end
and the rspec test would be like this expect(response).to be_not_found
but i dont want to do that (rescue ActiveRecord::RecordNotFound => e) in every single function (update, create, destroy and so on)
there is another way?
for example this way
rescue_from ActiveRecord::RecordNotFound, with: :not_found
def not_found
respond_to do |format|
format.json { head :not_found }
end
end
but i dont know how can i test with that
i would like to test the same way
expect(response).to be_not_found
I think that your original implementation with an error node is a better response but your modified change is a better way to handle so I would suggest combining the 2 concepts via
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def not_found(exception)
respond_to do |format|
format.json do
# I expanded the response a bit but handle as you see fit
render json: {
error: {
message: 'Record Not Found',
data: {
record_type: exception.model,
id: exception.id
}
}
}, status: :not_found
end
end
end
You should be able to maintain your current test in this case while avoiding the need to individually handle in each request.
You can add the below code in your application_controller.rb.
around_filter :catch_not_found #=> only:[:show, :edit]
def catch_not_found
yield
rescue ActiveRecord::RecordNotFound => e
respond_to do |format|
format.json { render json: { error: e.to_s }, status: :not_found and return }
format.html { redirect_to root_url, :flash => { :error => "Record not found." } and return }
end
end
Below is the simple example for test cases using RSpec. Modify as per your requirements.
staff_controller.rb
def show
#staff = Staff.find(params[:id])
end
RSpec
let(:staff) { FactoryBot.create(:staff) }
describe "GET #show" do
it "Renders show page for valid staff" do
get :show, {:id => staff.to_param}
expect(response).to render_template :show
end
it "redirects to root path on staff record not_found" do
get :show, id: 100
expect(response).to redirect_to(root_path)
end
end
My controller's #create action fails because of uniqueness constraint I've added to to the name and title attribute of my Boo model.
def create
#boo = current_user.my_boos.create(boo_params)
respond_to do |format|
if #boo.save
format.html { redirect_to root_path, notice: "Thanks!" }
format.json { render :index, status: :created, location: #boo }
else
format.html { render :new }
format.json { render json: #boo.errors, status: :unprocessable_entity }
end
end
end
Submitting with this action gives a Railsy looking PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_boos_on_name_and_title"error, which I think would be better displayed as an error message and a re-rendered page.
Everything I'm reading says that rescuing from Exception is bad, while rescuing from StandardError is good. But I've yet to find anything that explains how to display that error more nicely for the end user without rescuing from Exception. Like, it doesn't seem that StandardError works with the db.
The direct answer is to rescue the specific exception you want to recover from, and then handle it as you see fit... something like:
def create
#boo = current_user.my_boos.new(boo_params)
respond_to do |format|
begin
if #boo.save
format.html { redirect_to root_path, notice: "Thanks!" }
format.json { render :index, status: :created, location: #boo }
else
format.html { render :new }
format.json { render json: #boo.errors, status: :unprocessable_entity }
end
rescue PG::UniqueViolation
format.html { render :new }
format.json { render json: ["We've already got one"], status: :unprocessable_entity }
end
end
end
(rescuing StandardError there should work just as well, but while safe, it's much broader than we need.)
However, I'd suggest that a more "Railsy" solution is to define a uniqueness validation in your model, in addition to the DB constraint, so it'll be handled by the existing if #boo.save conditional.
You can add validation to your Boo model, it'll prevent from trying to save non-valid record and there will be no need to rescue from PG::UniqueViolation error:
class Boo < ApplicationRecord
# ...
validates :name, uniqueness: { scope: :title }
# ...
end
(c) http://guides.rubyonrails.org/active_record_validations.html#uniqueness
Let's say I have a student Rails API which having an endpoint that looks like http://www.example.com/students/1
What is the preferred way to implement?
review = Review.find(inputs[:review_id])
To handle exceptions,
rescue_from Exception, :with => :internal_error
def internal_error(e)
render json: {error: {message: "Internal Error"} }, :status => 500
end
OR
review = Review.where(inputs[:review_id]).first
if review.nil?
render json: {error: {message: "Internal Error"} }, :status => 500
end
My question is which is better way for handling non-existent id through the url.
You should go with the first approach
# reviews_controller.rb
review = Review.find(inputs[:review_id])
And
# application_controller.rb
# rescue_from Exception, :with => :internal_error
# OR Prefer ActiveRecord::RecordNotFound
rescue_from ActiveRecord::RecordNotFound, :with => :internal_error # Prefer this one
def internal_error(e)
render json: {error: {message: "Internal Error"} }, :status => 500
end
To make it generic, Add it to application_controller.rb
NOTE:
This way you don't have to rescue it in every controller (the second approach you have to)
You can add a global rescue_from in your base controller (ApplicationController for example) and then use the find method (Best way to retrieve only one record) :
rescue_from ActiveRecord::RecordNotFound do |e|
render status: :not_found, json: { error: { message: e.message } }
end
Every time you try to retrieve a record, if he doesn't exist you will render an error message and a 404 status which stand for a non-existent resource.
You should use rescue for manage error
def action_name
review = Review.find(inputs[:review_id])
render json: review, status: :ok
rescue # for ever not found
render json: {}, status: :not_found,nothing: true
end
doc for status list
and you can use rescue_from on header but this works for every action
rescue_from ActiveRecord::RecordNotFound,with: action_name
Neither. You can just do something like:
unless review = Review.find_by(id: inputs[:review_id])
render json: {error: {message: "record not found"} }, status: :not_found
end
Benefits:
Does't require endless nil checking as mentioned in comments.
Avoids unnecessary exception handling.
Returns a more informative error message.
I'm using Doorkeeper with rails-api to implement oAuth2 with password workflow:
resource_owner_from_credentials do
FacebookAuthorization.create(params[:username])
end
Right now when exception occurs it displays 500 template html response from rails. What I want to do is rescue any unexpected exception, then I want to custom the response error message base on exception that occurred in json response.
as the classes defined within the doorkeeper API will extend the Application controller, we can define the following in the Application controller
unless Rails.application.config.consider_all_requests_local
rescue_from Exception, with: :render_500
rescue_from ActionController::RoutingError, with: :render_404
rescue_from ActionController::UnknownController, with: :render_404
rescue_from ActionController::UnknownAction, with: :render_404
rescue_from ActiveRecord::RecordNotFound, with: :render_404
end
private
#error handling
def render_404(exception)
#not_found_path = exception.message
respond_to do |format|
format.html { render file: 'public/404.html', status: 404, layout: false }
format.all { render nothing: true, status: 404 }
end
end
def render_500(exception)
#error = exception
respond_to do |format|
format.html { render file: 'public/500.html', status: 500, layout: false }
format.all { render nothing: true, status: 500}
end
end
Then you can define the errors specifically in the ErrorsController
class ErrorsController < ActionController::Base
def not_found
if request.url.match('api')
render :json => {:error => "Not Found"}.to_json, :status => 404
else
render file: 'public/404.html', :status => 404, layout: false
end
end
def exception
if request.url.match('api')
render :json => {:error => "Internal Server Error"}.to_json, :status => 500
else
render file: 'public/500.html', :status => 500, layout: false
end
end
end
Hope this helps.
The following code sample is part of a Rails 3.2.16 app running on Ruby 1.9.3p484.
Whenever a new location is created or one is updated a message should be sent as defined in the after_filter.
class LocationController < InheritedResources::Base
respond_to :json
after_filter :notify_location_contact, only: [:create, :update]
def create
#location.user = current_user if current_user
create!
end
def update
update!
end
private
def notify_location_contact
message = MailForm.new
deliver_location_message(message)
end
def deliver_location_message(location_message)
begin
if location_message.deliver
render json: { message: "Successfully delivered" }, status: 201
else
render json: { error: "Delivery failure" }, status: 500
end
rescue => e
if e.is_a?(ArgumentError)
render json: { error: "Invalid Recipient" }, status: 422
else
render json: { error: e.message }, status: 500
end
end
end
end
The message itself is sent. Though, deliver_location_message first renders the "Successfully delivered" block and after the last block rendering the error message. This causes an internal server error:
Completed 500 Internal Server Error
AbstractController::DoubleRenderError - Render and/or redirect were
called multiple times in this action. Please note that you may only
call render OR redirect, and at most once per action. Also note that
neither redirect nor render terminate execution of the action, so if
you want to exit an action after redirecting, you need to do something
like "redirect_to(...) and return".
For sending the message the mail_form gem ~> 1.5.0 is used.
The DoubleRenderError seems to happen because create and update both render the JSON response when they finished there work. After, .deliver renders its JSON response to inform about success or failure.
As the error points out you need to return after calling render because you have multiple calls to render in your deliver_location_message(message) method. The reason for the error is because Rails continues execution until the end of the method regardless of render or redirect.
Please try the following. Note the return on each render line.
def deliver_location_message(message)
begin
if message.deliver
# Here
return render json: { message: "Successfully delivered" }, status: 201
else
# Here
return render json: { error: "Delivery failure" }, status: 500
end
rescue => e
if e.is_a?(ArgumentError)
# Here
return render json: { error: "Invalid Recipient" }, status: 422
else
# Here
return render json: { error: e.message }, status: 500
end
end
end
Alternative syntax to:
return render json: { message: "Successfully delivered" }, status: 201
is:
render json: { message: "Successfully delivered" }, status: 201 and return