I'm trying not to save a record in the database if an exception is raised but for some reason it is ignored. The record save anyway. What am I doing wrong? Any help is appreciated. Thanks!
# This is my controller
# POST /users
def create
service_action(success: #support_user, fail: :new) do |service|
service.create_user
end
end
def service_action(page)
result = yield(SupportUserService.new(#support_user, App.logger))
if result[:ok]
redirect_to page[:success], :notice => result[:message]
else
flash[:error] = result[:message] if result[:message].present?
render page[:fail]
end
end
#This is in a service class -> SupportUserService
def create_user
return_hash(:create) do |h|
if user.save
grant_ssh_access(user.login, user_ssh_keys!)
h[:ok] = true
h[:message] = "Support User '#{user.login}' was successfully created."
end
end
end
def return_hash(action)
{ok: false, message: ''}.tap do |h|
begin
yield h
rescue => e
h[:ok] = false
h[:e] = e
h[:message] = "Failed to #{action} support user: #{e.message}"
logger.error(stacktrace("Failed to #{action} support user", e))
end
end
end
Replace #save with exception raise version #save!, and remove if clause, so you'll get:
return_hash(:create) do |h|
user.save!
grant_ssh_access(user.login, user_ssh_keys!)
h[:ok] = true
h[:message] = "Support User '#{user.login}' was successfully created."
end
This will throw an exception on save failure, and you will be able to trap it in the #return_hash.
I fixed this by moving the grant_ssh_access function outside of the user.save method. That way if that function returns an exception it gets trapped before reaching the save method and exits.
Related
I encountered an issue today I haven't seen before - I have a custom validation to check if a discount code has already been used in my Order model:
validate :valid_discount_code
def valid_discount_code
is_valid = false
code = nil
if discount_code.present?
if discount_code.try(:downcase) == 'xyz'
code = 'xyz'
is_valid = true
else
code = Coupon.find_by_coupon_code(discount_code)
is_valid = code.present?
end
if code.nil?
is_valid = false
errors.add(:discount_code, "is not a valid referral code")
elsif ( code.present? && Coupon.where(email: email, coupon_code: code).present? )
is_valid = false
errors.add(:discount_code, "has already been used.")
end
puts "------------"
puts errors.full_messages ## successfully 'puts' the correct error message into my console.
puts "------------"
if is_valid
.... do stuff.....
end
end
end
In my controller:
if current_order.update_attributes(discount_code: params[:coupon_code].downcase, another_attribute: other_stuff)
....
session[:order_id] = nil
render json: { charged: 'true' }.as_json
else
puts "==============="
puts current_order.id # shows the correct current order ID
puts current_order.errors.full_messages # shows that there are no errors present
puts "==============="
render json: { charged: 'false', errors: current_order.errors.full_messages }.as_json
end
So it looks like at update_attributes, it runs the validation, fails the validation, creates the error message, and then once it's back at my controller the error message is gone. I'm stumped as to what can be causing that issue.
EDIT:
Here is what current_order is:
In ApplicationController.rb:
def current_order
session[:order_id].present? ? Order.find(session[:order_id]) : Order.new
end
Looks like every time you call current_order it reruns the find method. You can confirm this in the logs, but try not to call that, or at least memoize it. In an instance variable, the same order will be used everytime.
def current_order
#current_order ||= (Order.find_by_id(session[:order_id]) || Order.new)
end
I have a fairly straightforward if else statement in a controller as follows:
if citation_array.blank?
flash.now[:error] = "There was a problem saving the publications selected!"
#user = current_user
render 'pubmed_search'
else
citation_array.each do |user_publication|
begin
publication = Publication.new
render_publication(user_publication)
publication.citation = user_publication
publication.user_id = current_user.id
publication.title = #title
publication.authors = #authors
publication.journal = #journal
publication.year = #year
publication.volume = #volume
publication.pages = #pages
if publication.save
next
end
rescue
next
end
end
#user = current_user
redirect_to current_user
return false
end
It is served an array of id's in citation_array and if there are values present it loops throught them saving each publication found by the id's in the array. The render_publication method instantiates the instance variables so don't be concerned with that.
My issue is this. Very rarely an id is fake or wrong and so this block fails at that point. I want to simple move on to the next id in the array and forget about the failed id. I don't even need to save an exception. I'm new to Ruby (coming from a PHP background).
I want to check if this syntax is correct. I am having trouble checking it in the rails console.
Syntax errors are easier to spot if the code is indented correctly.
if citation_array.blank?
flash.now[:error] = "There was a problem saving the publications selected!"
#user = current_user
render 'pubmed_search'
else
citation_array.each do |user_publication|
begin
publication = Publication.new
render_publication(user_publication)
publication.citation = user_publication
publication.user_id = current_user.id
publication.title = #title
publication.authors = #authors
publication.journal = #journal
publication.year = #year
publication.volume = #volume
publication.pages = #pages
if publication.save
next
end
rescue
next
end
end
#user = current_user
redirect_to current_user
return false
end
The syntax seems correct. Though an easier way to find out would have been just to run the code.
Some things in the code are not necessary though. After cleaning up your code a bit, it would look something like this with the same functionality.
#user = current_user
if citation_array.blank?
flash.now[:error] = 'There was a problem saving the publications selected!'
render 'pubmed_search'
else
citation_array.each do |user_publication|
begin
render_publication(user_publication)
Publication.create!( # create! here so that if something does go wrong, then you're not just ignoring it, but you can log it in your rescue block.
citation: user_publication,
user_id: current_user.id,
title: #title,
authors: #authors,
journal: #journal,
year: #year,
volume: #volume,
pages: #pages
# This hash should be extracted to a method.
)
rescue
# Just doing nothing here is valid syntax, but you should at least log your error.
end
end
redirect_to current_user
false # This can most likely be omitted as well since not many places care about the return value of a controller action.
end
Syntax for begin-rescue,
begin
your code...
rescue => e
Rails.logger.debug 'Exception is #{e}'
end
Here is our code to save quite a few objects all at once within one transaction. What the code does is to create a new checkout record (for warehouse) and update each item (there may be a few of them) in stock. Since all save has to be either all or none, we put all the save within Rails transaction:
#checkout = RequisitionCheckoutx::Checkout.new(params[:checkout])
#checkout.last_updated_by_id = session[:user_id]
#checkout.checkout_by_id = session[:user_id]
#checkout.transaction do
params['ids'].each do |id|
params['out_qtys'].each do |qty| #ids passed in as a string of 'id'
stock_item = RequisitionCheckoutx.warehouse_class.find_by_id(id.to_i)
qty = qty.to_i
if stock_item.stock_qty >= qty
stock_item.stock_qty = stock_item.stock_qty - qty
stock_item.last_updated_by_id = session[:user_id]
begin
stock_item.save
rescue => e
flash[:notice] = t('Stock Item#=') + id.to_s + ',' + e.message
end
end
end unless params['out_qtys'].blank?
end unless params['ids'].blank?
if #checkout.save
redirect_to URI.escape(SUBURI + "/authentify/view_handler?index=0&msg=Successfully Saved!")
else
flash[:notice] = t('Data Error. Not Saved!')
render 'new'
end
end
We haven't run the test yet and the code looks not pretty. Is there a better way to handle this kind of batch save? Also should the rescue loop be removed for transaction?
The transaction block should be performed first and then you should deal with action response. Besides that, catching exception here is pointless, cause using save returns simply true or false. Your transaction should look like:
RequisitionCheckoutx::Checkout.transaction do
begin
#...
#...
stock_item.save! # it will raise RecordInvalid or RecordNotSaved if something goes wrong
#...
#...
#checkout.save!
rescue Exception => e
raise ActiveRecord::Rollback # it calls Rollback to the database
end
end
Now, using ActiveModel::Dirty you need to check if #checkout has been saved:
if !#checkout.changed?
redirect_to "/something"
else
flash[:notice] = t('Data Error. Not Saved!')
render 'new'
end
The problem is how can I catch exception in delivering mail by ActionMailer. For me it sounds impossible, because in this case ActionMailer should sent mail to mailserver, and if mailserver returns error, ActionMailer should show me this error. I am interested only in counting undelivered mails.
Do you have any ideas how to implement this?
Thanks!
I'm using something like this in the controller:
if #user.save
begin
UserMailer.welcome_email(#user).deliver
flash[:success] = "#{#user.name} created"
rescue Net::SMTPAuthenticationError, Net::SMTPServerBusy, Net::SMTPSyntaxError, Net::SMTPFatalError, Net::SMTPUnknownError => e
flash[:success] = "Utente #{#user.name} creato. Problems sending mail"
end
redirect_to "/"
This should work in any of your environment files. development.rb or production.rb
config.action_mailer.raise_delivery_errors = true
class ApplicationMailer < ActionMailer::Base
rescue_from [ExceptionThatShouldBeRescued] do |exception|
#handle it here
end
end
Works with rails 5 onwards. And it's the best practice.
If you are sending a lot of emails, you can also keep your code more DRY and get notifications of exceptions to your email by doing something like this:
status = Utility.try_delivering_email do
ClientMailer.signup_confirmation(#client).deliver
end
unless status
flash.now[:error] = "Something went wrong when we tried sending you and email :("
end
Utility Class:
class Utility
# Logs and emails exception
# Optional args:
# request: request Used for the ExceptionNotifier
# info: "A descriptive messsage"
def self.log_exception(e, args = {})
extra_info = args[:info]
Rails.logger.error extra_info if extra_info
Rails.logger.error e.message
st = e.backtrace.join("\n")
Rails.logger.error st
extra_info ||= "<NO DETAILS>"
request = args[:request]
env = request ? request.env : {}
ExceptionNotifier::Notifier.exception_notification(env, e, :data => {:message => "Exception: #{extra_info}"}).deliver
end
def self.try_delivering_email(options = {}, &block)
begin
yield
return true
rescue EOFError,
IOError,
TimeoutError,
Errno::ECONNRESET,
Errno::ECONNABORTED,
Errno::EPIPE,
Errno::ETIMEDOUT,
Net::SMTPAuthenticationError,
Net::SMTPServerBusy,
Net::SMTPSyntaxError,
Net::SMTPUnknownError,
OpenSSL::SSL::SSLError => e
log_exception(e, options)
return false
end
end
end
Got my original inspiration from here: http://www.railsonmaui.com/blog/2013/05/08/strategies-for-rails-logging-and-error-handling/
This is inspired by the answer given by #sukeerthi-adiga
class ApplicationMailer < ActionMailer::Base
# Add other exceptions you want rescued in the array below
ERRORS_TO_RESCUE = [
Net::SMTPAuthenticationError,
Net::SMTPServerBusy,
Net::SMTPSyntaxError,
Net::SMTPFatalError,
Net::SMTPUnknownError,
Errno::ECONNREFUSED
]
rescue_from *ERRORS_TO_RESCUE do |exception|
# Handle it here
Rails.logger.error("failed to send email")
end
end
The * is the splat operator. It expands an Array into a list of arguments.
More explanation about (*) splat operator
I have a create method that calls a method in a model that pings some third-party APIs.
What I need to do is if the API sends back a certain message, then I'd display an error.
Below is my current controller and model setup, so how would I get the error back in to the controller (and ultimately the view)?
Here is the method in my controller:
def create
#number = Number.where(:tracking => params[:number][:tracking], :user_id => current_user.id).first
if #number.blank?
#number = Number.new
#number.tracking = params[:number][:tracking]
#number.user_id = current_user.id
#number.notes = params[:number][:notes]
#number.track
end
respond_with(#number) do |format|
format.html { redirect_to root_path }
end
end
Here are the methods in my model:
def track
create_events
end
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
self.assign_tracker(tracker)
tracker.events.each do |e|
self.create_event(e) unless (block_given? && !block.call(e))
end
save
else
# NEED TO THROW THE ERROR HERE
end
end
How about if rather than throwing errors, you just use validation? Something like the following (Just to get your started. This would need work.):
# if you don't cache the tracker in an attribute already, do this so
# you can add errors as if it were a column.
attr_accessor :tracker
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
# ...
else
# add the error with i18n
errors.add(:tracker, :error_type_if_you_know_it)
# or add it from a returned message
errors.add(:tracker, nil, :message => fedex.get_error())
end
end
Then in your controller:
#number.track
respond_with(#number) do |format|
if #number.errors.any?
format.html { redirect_to root_path }
else
format.html { render :some_template_with_errors }
end
end
Alternatively you could do this as part of validation (so calling valid? would work as expected and not destroy your custom "track" errors)
# do your tracking on creation, if number was given
validate :on => :create do
if number.present?
tracker = fedex.track(:tracking_number => number)
unless tracker.valid?
errors.add :tracker, nil, :message => tracker.get_error()
end
end
end
# then do your actual creation of tracking events sometime after validation
before_save :handle_tracker_assignment
def handle_tracker_assignment
self.assign_tracker(tracker)
# note the block method you're using would need to be reworked
# ...
end
Note in the latter case you'd have to change your logic a bit, and simply pass the tracking number and attempt to save a new record, which would trigger the tracking attempt.
You should typically offload the the API calls to a background job and you could either use notifiers (or Rack middleware) to raise self-defined errors and handle them accordingly.