I Accidentally the Whole SMTP Exception

Mike Burns

You have a slick, exclusive, invite-only Web app for sharing Tor URLs, with an Android client and specialty hardware. You use validatesemailformat_ofin the Invitation model, but still something slips through and your Hoptoad errors pile up, showing your user the beautifully-designed 500 page instead of an error explanation.

There are two types of exceptions that ActionMailer will raise when you attempt to deliver an email: user input problems and server problems.

User input problems are those such as incorrect or invalid email addresses; the exceptions raised are Net::SMTPFatalError and Net::SMTPSyntaxError. These are issues that the user can fix and as such the error message should indicate that everything’s fine, nothing is ruined.

Server problems could be anything from a non-existent server to an authentication issue; the exceptions raised are: TimeoutError, IOError, Net::SMTPUnknownError, Net::SMTPServerBusy, and Net::SMTPAuthenticationError. These issues are outside the power of the user and should indicate that we screwed up.

So in config/initializers/errors.rb:

SMTP_SERVER_ERRORS = [
  IOError,
  Net::SMTPAuthenticationError,
  Net::SMTPServerBusy,
  Net::SMTPUnknownError,
  TimeoutError,
]

SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, Net::SMTPSyntaxError]

SMTP_ERRORS = SMTP_SERVER_ERRORS.concat(SMTP_CLIENT_ERRORS)

SMTP_CLIENT_ERROR_FLASH = 'The email address supplied is invalid. Please
  check for spelling mistakes.'
SMTP_SERVER_ERROR_FLASH = 'We encountered an internal issue while attempting
  to deliver this email. Please try again in a few minutes.'

We can test it with invitations_controller_test.rb:

class InvitationsController; def rescue_action(e) raise e end; end

class InvitationsControllerTest < Test::Unit::TestCase
  SMTP_CLIENT_ERRORS.each do |exn|
    should "handle #{exn}" do
      InvitationsMailer.expects(:deliver_invitation).raises(exn)
      post :create, :invitation => {:email => 'invalid email'}
      assert_match /#{SMTP_CLIENT_ERROR_FLASH}/i, @response.flash[:warning]
      assert_template 'new'
    end
  end

  SMTP_SERVER_ERRORS.each do |exn|
    should "handle #{exn}" do
      InvitationsMailer.expects(:deliver_invitation).raises(exn)
      post :create, :invitation => {:email => 'invalid email'}
      assert_match /#{SMTP_SERVER_ERROR_FLASH}/i, @response.flash[:warning]
      assert_template 'new'
    end
  end
end

And in invitations_controller.rb:

class InvitationsController < ApplicationController
  def create
    @invitation.new(params[:invitation])
    if @invitation.save
      redirect_to root_url
    else
      render :action => 'new'
    end
  rescue *SMTP_CLIENT_ERRORS
    flash[:warning] = SMTP_CLIENT_ERROR_FLASH
    render :action => 'new'
  rescue *SMTP_SERVER_ERRORS => error
    notify_hoptoad error
    flash[:warning] = SMTP_SERVER_ERROR_FLASH
    render :action => 'new'
  end
end

If you use Suspenders you’ll be pleased to find that we’ve included config/initializers/errors.rb for you pre-populated with both SMTP and HTTP exceptions.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.