ActiveCampaign/postmark-rails

How to handle exceptions?

alexhanh opened this issue · 11 comments

Related #19 and #35

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.

@temochka thank you

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.