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
Related
I am trying to make my stripe webhook work, using the request.raw_post method instead of the request.body.read method. I am still getting this error:
Signature error
#<Stripe::SignatureVerificationError: No signatures found matching the expected signature for payload>
Also, I am getting only "200" messages in my terminal logs when running stripe trigger checkout.session.completed in my terminal so everything is supposed to work.
This is my Webhooks Controller:
class WebhooksController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
unless has_header? "RAW_POST_DATA"
raw_post_body = body
set_header("RAW_POST_DATA", raw_post_body.read(content_length))
raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
end
get_header "RAW_POST_DATA"
end
def create
payload = request.raw_post.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, Rails.application.credentials[:stripe][:webhook]
)
rescue JSON::ParserError => e
status 400
# Invalid payload
puts "Payload error"
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
puts "Signature error"
p e
return
end
# Handle the event
case event.type
when 'checkout.session.completed'
booking = Booking.find_by(checkout_session_id: event.data.object.id)
booking.update(paid: true)
booking.save
# #booking = Booking.where(checkout_session_id: event.data.object.id)
# #booking.update(paid: true)
# #booking.save
end
render json: { state: "processed" }, status: :ok
end
end
In my webhooks logs on stripe, every event is working except checkout.session.completed where I get a 500 error.
I have been struggling with this error for a while now so I would really appreciate any help :)
I am using Mailchimp (via the Gibbon gem) to add email addresses to my Mailchimp mailing list, and I want to handle any errors that are returned by Mailchimp and display them in my view.
Here is my Pages controller:
class PagesController < ApplicationController
def subscribe
email = subscriber_params[:email]
if email.empty?
flash[:error] = 'Please provide an email.'
redirect_to root_path
else
subscriber = Mailchimp.new.upsert(email)
if subscriber
flash[:success] = 'You\'re in!'
redirect_to root_path(subscribed: :true)
else
# Return error coming from Mailchimp (i.e. Gibbon::MailChimpError)
end
end
end
end
And here is the app > services > mailchimp.rb file I set up to separate out the Mailchimp logic:
class Mailchimp
def initialize
#gibbon = Gibbon::Request.new(api_key: Rails.application.credentials.mailchimp[:api_key])
#list_id = Rails.application.credentials.mailchimp[:list_id]
end
def upsert(email)
begin
#gibbon.lists(#list_id).members.create(
body: {
email_address: email,
status: "subscribed"
}
)
rescue Gibbon::MailChimpError => e #This is at the bottom of the Gibbon README
raise e.detail
end
end
end
What I'm trying to figure out is how to return/send Gibbon::MailChimpError back to my Pages#subscribe action. I see it being outputted as a RuntimeError in my console, but I'm not sure the right way to access/pass it along.
And please let me know if there's a better practice for this kind of implementation.
You could move the begin/rescue block to the subscribe action inside your controller to handle the error from there, or even better, you can use rescue_from in your controller like this:
class PagesController < ApplicationController
rescue_from Gibbon::MailChimpError do |e|
# Handle the exception however you want
end
def subscribe
# ....
end
end
I recently started getting complaints that locked users cannot reset their accounts from the email I am sending.
I'm getting
NameError (uninitialized constant Unlock):
config/initializers/quiet_assets.rb:6:in `call_with_quiet_assets'
Nothing has changed on my routesr and nothing has changed in my links... what could be causing this error message and how can I fix it?
quiet_assets.rb:
Rails.application.assets.logger = Logger.new('log/logger.txt')
Rails::Rack::Logger.class_eval do
def call_with_quiet_assets(env)
previous_level = Rails.logger.level
Rails.logger.level = Logger::ERROR if env['PATH_INFO'].index("/assets/") == 0
call_without_quiet_assets(env).tap do
Rails.logger.level = previous_level
end
end
alias_method_chain :call, :quiet_assets
end
Stacktrace:
INFO Started GET "/users/unlock?unlock_token=CzpxHwV5kL7EyDZb32Ex" for 127.0.0.1 at 2018-11-22 21:08:56 +0200
INFO Processing by Devise::UnlocksController#show as HTML
INFO Parameters: {"unlock_token"=>"CzpxHwV5kL7EyDZb32Ex"}
INFO -- store_location: /users/unlock?unlock_token=CzpxHwV5kL7EyDZb32Ex
INFO Completed 500 Internal Server Error in 8.1ms
FATAL NameError (uninitialized constant Unlock):
config/initializers/quiet_assets.rb:6:in `call_with_quiet_assets'
INFO Rendered /Users/david/.rvm/gems/ruby-1.9.3-p551/gems/actionpack-3.2.17/lib/action_dispatch/middleware/templates/rescues/_trace.erb (1.7ms)
INFO Rendered /Users/david/.rvm/gems/ruby-1.9.3-p551/gems/actionpack-3.2.17/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (0.8ms)
Controller
require 'white_label_utils'
include ERB::Util
#Need to include these helpers or image_path won't work
include Sprockets::Helpers::RailsHelper
include Sprockets::Helpers::IsolatedHelper
include ActionView::Helpers::AssetTagHelper
class CustomMailer < Devise::Mailer
include Devise::Mailers::Helpers
include Devise::Controllers::UrlHelpers # Optional. eg. `confirmation_url`
helper :application # gives access to all helpers defined within `application_helper`.
default from: 'no-reply#ourcompany.com'
# Sends a simple email (without attachments and call to action button)
# to - array of recipients
# subject - subject title of email
# message - body of email
# section - section used for white labeling purposes
# file_attachments - array of files attachments to be included in email. eg. [{name: 'myFileName.csv', content: fileData}, {name: 'anotherFileName.zip', content: anotherFileData}] Empty by default
def simple_email(to, subject, message, section = nil, file_attachments = [])
get_whitelabel_details(section)
set_email_global_params(to, subject, message)
# if there are files, attach them
file_attachments = [] if file_attachments.nil?
if file_attachments.length > 0
file_attachments.each do |file|
attachments["#{file[:name]}"] = file[:content]
end
end
mail(to: to, subject: #subject, from: #whitelabel.email_from).delivery_method.settings.merge!(Dynamic::Application.config.send_grid_smtp_settings)
end
# Sends a devise invitation_instructions email located under app/views/user/mailer/invitation_instructions.html
def invitation_instructions(record, opts = {})
to = record.email
begin
section = record.publisher.sections.first
rescue => e
Rails.logger.warn "Could not find any sections associated with user #{record}"
section = nil
end
initialize_from_record(record)
get_whitelabel_details(section)
set_email_global_params(to, invitation_email_subject, I18n.t("devise.mailer.invitation_instructions.message", invitee_name: record.first_name.capitalize, inviter_name: #resource.invited_by.is_dy_admin ? #resource.invited_by.last_name.capitalize : #resource.invited_by.full_name, product_name: #whitelabel.product_name))
uri = URI.parse(edit_invitations_url({invitation_token: #resource.invitation_token}))
#cta_text = I18n.t("devise.mailer.invitation_instructions.call_to_action_text")
#cta_link = whitelabel_links(uri)
super record, opts
end
# Sends a devise unlock_instructions email located under app/views/user/mailer/unlock_instructions.html
def unlock_instructions(record, opts = {})
to = record.email
begin
section = record.publisher.sections.first
rescue => e
Rails.logger.warn "Could not find any sections associated with user #{record}"
section = nil
end
initialize_from_record(record)
get_whitelabel_details(section)
set_email_global_params(to, I18n.t("devise.mailer.unlock_instructions.subject"), I18n.t("devise.mailer.unlock_instructions.message"))
uri = URI.parse(unlock_url(#resource, :unlock_token => #resource.unlock_token))
#cta_text = I18n.t("devise.mailer.unlock_instructions.call_to_action_text")
#cta_link = whitelabel_links(uri)
super record, opts
end
# Sends a devise reset_password_instruction email located under app/views/user/mailer/reset_password_instructions.html
def reset_password_instructions(record, token = nil, opts = {})
to = record.email
begin
section = record.publisher.sections.first
rescue => e
Rails.logger.warn "Could not find any sections associated with user #{record}"
section = nil
end
initialize_from_record(record)
get_whitelabel_details(section)
set_email_global_params(to, I18n.t("devise.mailer.reset_password_instructions.subject"), I18n.t("devise.mailer.reset_password_instructions.message"))
uri = URI.parse(edit_password_url(#resource, {reset_password_token: #resource.reset_password_token}))
#cta_text = I18n.t("devise.mailer.reset_password_instructions.call_to_action_text")
#cta_link = whitelabel_links(uri)
super record, opts
end
private
def devise_mail(record, action, opts={})
initialize_from_record(record)
(mail headers_for(action, opts)).delivery_method.settings.merge!(Dynamic::Application.config.send_grid_smtp_settings)
end
# Customize the subject and sender display name by the white-label profile
def headers_for(action, opts)
headers = {:from => #email_from,
:reply_to => #email_from}
super.merge!(headers)
end
# Overrides the default subject line for devise emails (reset_password_instructions, invitation_instructions, etc)
def subject_for(key)
return super unless key.to_s == 'invitation_instructions'
invitation_email_subject
end
# Gets the whitelabel details associated with the section
def get_whitelabel_details(section)
#section = section.blank? ? err_message('section') : section
begin
#whitelabel = WhiteLabelUtils::get_profile(nil, section.site.publisher)
rescue => e
Rails.logger.warn "Could not determine WhiteLabel profile when sending email"
#whitelabel = WhiteLabelUtils::get_profile(nil, nil)
end
end
# Validates the existence of parameters and assigns them to global vars that will be used in the email template itself
def set_email_global_params(to, subject, message)
#errors = nil
#to = to.blank? ? err_message('to') : to
#subject = subject.blank? ? err_message('subject') : subject.slice(0, 1).capitalize + subject.slice(1..-1).chomp('.') #remove trailing period, we add this in the template so this avoids duplicates
#message = message.blank? ? err_message('message') : message.slice(0, 1).capitalize + message.slice(1..-1).chomp('.') #remove trailing period, we add this in the template so this avoids duplicates
#email_from = #whitelabel.email_from
#reply_to = #whitelabel.reply_to
#introduction = create_introduction(to)
unless #errors.blank?
raise #errors
end
end
def invitation_email_subject
I18n.t("devise.mailer.invitation_instructions.subject", product_name: #whitelabel.product_name)
end
# Receives a generic url and replaces it with whitelabelled domains
def whitelabel_links uri
"#{#whitelabel.root_url_info[:protocol]}://#{#whitelabel.root_url_info[:host]}#{uri.path}?#{uri.query}"
# rendered as https://companydomain.com/users/unlock?unlock_token=TOKENVALUE
end
# Searches the system for a user with 'email_address' and returns
# a personalized introduction with the user's first name otherwise
# returns a generic introduction with the wording 'Dear User'
def create_introduction email_address
user = User.where(email: email_address).first
"#{I18n.t("dy.common.general.hi")}#{user.nil? ? '' : " #{user.first_name.capitalize}"},"
end
def err_message(val)
#errors = #errors.blank? ? '<' + val + '> field can not be empty' : #errors + '\n<' + val + '> field can not be empty'
end
end
The route being called is https://example.com/users/unlock?unlock_token=USERS_TOKEN
and the controller is from devise unlockscontroller
class Devise::UnlocksController < DeviseController
prepend_before_filter :require_no_authentication
# GET /resource/unlock/new
def new
build_resource({})
end
# POST /resource/unlock
def create
self.resource = resource_class.send_unlock_instructions(resource_params)
if successfully_sent?(resource)
respond_with({}, :location => after_sending_unlock_instructions_path_for(resource))
else
respond_with(resource)
end
end
# GET /resource/unlock?unlock_token=abcdef
def show
self.resource = resource_class.unlock_access_by_token(params[:unlock_token])
if resource.errors.empty?
set_flash_message :notice, :unlocked if is_navigational_format?
respond_with_navigational(resource){ redirect_to after_unlock_path_for(resource) }
else
respond_with_navigational(resource.errors, :status => :unprocessable_entity){ render :new }
end
end
protected
# The path used after sending unlock password instructions
def after_sending_unlock_instructions_path_for(resource)
new_session_path(resource) if is_navigational_format?
end
# The path used after unlocking the resource
def after_unlock_path_for(resource)
new_session_path(resource) if is_navigational_format?
end
end
This was never properly resolved. I ended overriding the built-in devise callback functions (which is a horrible practice)
Old versions of Rails3 and Ruby 1.9.3 are most probably the culprits - but I don't have the privilege of upgrading - too much legacy code working in production already.
Story of my life :(
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.
Rails 4 adds an exception ActionDispatch::ParamsParser::ParseError exception but since its in the middleware stack it appears it can't be rescued in the normal controller environment. In a json API application I want respond with a standard error format.
This gist shows a strategy for inserting middleware to intercept and respond. Following this pattern I have:
application.rb:
module Traphos
class Application < Rails::Application
....
config.middleware.insert_before ActionDispatch::ParamsParser, "JSONParseError"
end
end
And the middleware is:
class JSONParseError
def initialize(app)
#app = app
end
def call(env)
begin
#app.call(env)
rescue ActionDispatch::ParamsParser::ParseError => e
[422, {}, ['Parse Error']]
end
end
end
If I run my test without the middleware I get (spec):
Failures:
1) Photo update attributes with non-parseable json
Failure/Error: patch update_url, {:description => description}, "CONTENT_TYPE" => content_type, "HTTP_ACCEPT" => accepts, "HTTP_AUTHORIZATION" => #auth
ActionDispatch::ParamsParser::ParseError:
399: unexpected token at 'description=Test+New+Description]'
Which is exactly what I would expect (ParseError that I can't rescue_from).
Now with the only change to add in the middleware above:
2) Photo update attributes with non-parseable json
Failure/Error: response.status.should eql(422)
expected: 422
got: 200
And the log shows that the standard controller action is being executed and returning a normal response (although since it didn't receive any parameters it didn't update anything).
My questions:
How can rescue from ParseError and return a custom response. Feels like I'm on the right track but not quite there.
I can't work out why, when the exception is raised and rescued, that the controller action still proceeds.
Help much appreciated, --Kip
Turns out that further up the middleware stack, ActionDispatch::ShowExceptions can be configured with an exceptions app.
module Traphos
class Application < Rails::Application
# For the exceptions app
require "#{config.root}/lib/exceptions/public_exceptions"
config.exceptions_app = Traphos::PublicExceptions.new(Rails.public_path)
end
end
Based heavily on the Rails provided one I am now using:
module Traphos
class PublicExceptions
attr_accessor :public_path
def initialize(public_path)
#public_path = public_path
end
def call(env)
exception = env["action_dispatch.exception"]
status = code_from_exception(env["PATH_INFO"][1..-1], exception)
request = ActionDispatch::Request.new(env)
content_type = request.formats.first
body = {:status => { :code => status, :exception => exception.class.name, :message => exception.message }}
render(status, content_type, body)
end
private
def render(status, content_type, body)
format = content_type && "to_#{content_type.to_sym}"
if format && body.respond_to?(format)
render_format(status, content_type, body.public_send(format))
else
render_html(status)
end
end
def render_format(status, content_type, body)
[status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
'Content-Length' => body.bytesize.to_s}, [body]]
end
def render_html(status)
found = false
path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))
if found || File.exist?(path)
render_format(status, 'text/html', File.read(path))
else
[404, { "X-Cascade" => "pass" }, []]
end
end
def code_from_exception(status, exception)
case exception
when ActionDispatch::ParamsParser::ParseError
"422"
else
status
end
end
end
end
To use it in a test environment requires setting config variables (otherwise you get the standard exception handling in development and test). So to test I have (edited to just have the key parts):
describe Photo, :type => :api do
context 'update' do
it 'attributes with non-parseable json' do
Rails.application.config.consider_all_requests_local = false
Rails.application.config.action_dispatch.show_exceptions = true
patch update_url, {:description => description}
response.status.should eql(422)
result = JSON.parse(response.body)
result['status']['exception'].should match(/ParseError/)
Rails.application.config.consider_all_requests_local = true
Rails.application.config.action_dispatch.show_exceptions = false
end
end
end
Which performs as I need in a public API way and is adaptable for any other exceptions I may choose to customise.
This article (also from 2013) thoughtbot covers also this topic. They put their response inside this middleware service only if you requested json
if env['HTTP_ACCEPT'] =~ /application\/json/
error_output = "There was a problem in the JSON you submitted: #{error}"
return [
400, { "Content-Type" => "application/json" },
[ { status: 400, error: error_output }.to_json ]
]
else
raise error
end