How to handle exceptions?
alexhanh opened this issue · 11 comments
We are running on Rails 4.2.4. We are trying to figure out how to properly handle delivery errors (such as Postmark::InvalidMessageError
) properly, with and without ActiveJob (which, in our case, is backed by DelayedJob).
class UserMailer < ActionMailer::Base
def welcome(email)
mail to: email, subject: 'Welcome!'
end
end
Running UserMailer.welcome('doesnotexist@foo.com').deliver
will, of course, raise the Postmark::InvalidMessageError
. But how do we handle that? The other issues point to a flow where the mail
call is wrapped into a begin rescue
block, but that doesn't work because the mail
just returns the message object and the actual delivery is done somewhere else (thus the rescue doesn't catch anything).
Next, we tried
class UserMailer < ActionMailer::Base
around_action do |mailer, block|
begin
block.call
rescue Postmark::InvalidMessageError => e
puts "#{e}"
end
end
def welcome(email)
mail to: email, subject: 'Welcome!'
end
end
But still no luck. The reason is probably the same as before: block.call
will simply call welcome
which returns the message object and the delivery is actually done outside of this chain.
Wrapping the original call solves the problem:
begin
UserMailer.welcome('foo@bar.com').deliver
rescue Postmark::InvalidMessageError => e
puts "#{e}"
end
But we don't obviously want to wrap every call like this - and - in reality, we really use deliver_later
to push it via ActiveJob to the background (DelayedJob). This is currently the best solution we could come up with. It uses a simple proxy class:
class MailerJob < ActiveJob::Base
queue_as :default
# Example how to handle other errors
rescue_from(ActiveJob::DeserializationError) do |e|
end
def perform(message)
begin
message.deliver_now
rescue Postmark::InvalidMessageError => e
# somehow set the user(s) emails to inactive in the database
end
end
end
MailerJob.perform_later(UserMailer.welcome('foo@bar.com'))
This feels a bit patched and I'm certain we've overlooked a much simpler solution here. Please advice!
Update: Unfortunately the MailerJob approach doesn't work because ActiveJob doesn't know how to serialize ActionMailer::MessageDelivery
or Mail::Message
. I guess we could do something like MailerJob.perform_later(UserMailer, :welcome, 'foo@bar.com')
instead.
I just realised that we've probably been thinking this the wrong way around. You are probably supposed to have raise_delivery_errors = false
(which would not raise the Postmark exceptions) and do the syncing & cleaning of inactive addresses separately.
Hi @alexhanh! Right now, there is indeed no good way of handling delivery errors when using #deliver_later
. If ignoring delivery errors is an option for your app, then raise_delivery_errors = false
is a way to go. Otherwise, having a separate job that explicitly handles delivery errors is what we currently recommend. It’s true that you can’t serialize Mail::Message
objects generated by ActiveRecord unless you turn them into a raw message string. Unfortunately you will have to spend your request time on generating the message body, which is inefficient. That said, the MailerJob.perform_later(UserMailer, :welcome, 'foo@bar.com')
format is preferable.
I’ll keep this issue open, as we definitely need a better way of handling delivery errors in async context. I’m open to suggestions if you have any.
Got it, thank you for the response @temochka. Another possible idea would be to monkey patch ActionMailer::DeliveryJob#perform
, which is what deliver_later
uses under the hood (http://apidock.com/rails/v4.2.1/ActionMailer/MessageDelivery/enqueue_delivery). Or even more simply deliver_later
. Also, I wonder if send https://github.com/evendis/mandrill-rails or https://github.com/stephenb/sendgrid have ideas for a solution to handling these errors.
Could you kindly elaborate in which situations it wouldn't be possible to ignore deliver errors just to understand the different needs?
In situations when it’s critical for your app that not a single email could be lost due to network errors or addresses wrongly marked as inactive. By default the Postmark gem retries all connection and API server errors, but that’s not enough to ensure fail-proof delivery workflow. If you ignore exceptions raised by the gem, then if we have an outage (or your hosting provider is experiencing network issues) you may end up losing messages, just because the gem won’t be able to reach our servers. And you won’t even know about it.
Another use case is related to handling errors related to inactive addresses. A common workflow is to query the API for the latest bounce type. Then you analyze the bounce type and re-activate the address if an error was temporary (full mailbox, etc.) and retry the delivery.
To sum this up, it’s your app’s responsibility to handle "offline" mode and all Postmark API errors. Generally, if you’re using a background job processing system, it can automatically retry all failed jobs for you, so your delivery recovers automatically as the problem resolves. Depending on your setup, you might have to implement some parts of that logic yourself, but even if that’s the case, doing it is usually straightforward.
Has there been any progress with this issue? I'm currently planning just just monkey patch deliver_now
(used internally by deliver_later
) to rescue this particular error, but would like to avoid that if at all possible.
@SeriouslyAwesome I think you can resolve this with service object
`
class ProcessingMailsService
def initialize(klass, method, *args)
@klass = klass
@method = method
@Args = args
end
def perform()
begin
@klass.send(@method, *@Args).deliver_now
rescue Postmark::InvalidMessageError => e
# somehow set the user(s) emails to inactive in the database
end
end
end
ProcessingMailsService.new(MyMailer, :send_email, current_user.name, , current_user.email).delay.perform`
@temochka I have a question how i can receive "bounceid" to activate email after error thrown?
Hi @arfelio,
You can use the API to search for a bounce that deactivated that particular email address and reactivate it. Both calls (#get_bounces
and #activate_bounce
) are documented in the postmark gem README. The Rails gem depends on the postmark gem.
See the list of supported query parameters to narrow down the list of bounces to just the one that you need. You’ll probably want to use inactive=true
and emailFilter=someone@example.org
. The ID
parameter of the response is the identifier that you need.
Rails 5 has exception handling for Action Mailer via the usual rescue_from
syntax, including delegation back to the mailer's handlers for exceptions raised within #deliver_later
jobs.
For my Rails 4.2 app I just wanted to quiet the noise in the logs for inactive recipients, and get the exception data to Rollbar. I was content to just patch DeliveryJob with config/initializers/mail_delivery_exception_handling.rb
containing:
class ActionMailer::DeliveryJob
# We don't want to retry inactive addresses.
rescue_from(Postmark::InvalidMessageError) do |ex|
logger.error("Postmark error (#{ex.class} code #{ex.error_code}) on delivery attempt:\n#{ex.message}")
Rollbar.error(ex)
end
end
and something like this may work for you. Works with DelayedJob.
Thank you @inopinatus! Looks like ActionMailer::Base.rescue_from
covers most of the needs here. I took care of updating the README accordingly. I also documented a workaround that should work for Rails 4.2 users who rely on #deliver_later
.
As a side note: the postmark
gem version 1.11.0 comes with new, more helpful exception classes. The postmark-rails
version 0.16.0 with the updated dependency is coming in the next few days.