antiwork/gumroad · error · Installment::PreviewEmailError

Failed to send preview email. Please try again later.

Error message

Failed to send preview email. Please try again later.

What it means

Raised as Installment::PreviewEmailError by Installment#send_preview_email when PostEmailApi.process for the preview raises ResendApiResponseError or SendGridApiResponseError. Either ESP can be the one actually sending a given preview — the recipient's domain (MailerInfo::UNITED_INTERNET_RECIPIENT_DOMAINS) and a random split decide — so both failures are normalized into one friendly message instead of an unhandled 500. It signals a transient upstream provider failure, not a problem with the installment.

Source

Thrown at app/models/installment.rb:421

  end

  def send_preview_email(recipient_user)
    if recipient_user.has_unconfirmed_email?
      raise PreviewEmailError, "You have to confirm your email address before you can do that."
    elsif abandoned_cart_type?
      CustomerMailer.abandoned_cart_preview(recipient_user.id, id).deliver_later
    else
      recipient = { email: recipient_user.email }
      recipient[:url_redirect] = UrlRedirect.find_or_create_by!(installment: self, purchase: nil) if has_files?
      begin
        PostEmailApi.process(post: self, recipients: [recipient], preview: true)
      rescue ResendApiResponseError, SendGridApiResponseError
        # Either provider can be the one that actually sends this preview: the
        # recipient's domain decides (see
        # MailerInfo::UNITED_INTERNET_RECIPIENT_DOMAINS), as does the random
        # split, so a failure from either has to become the same friendly error
        # rather than an unhandled 500 on the preview request.
        raise PreviewEmailError, "Failed to send preview email. Please try again later."
      end
    end
  end

  def send_installment_from_workflow_for_purchase(purchase_id, reschedule_reference_time: nil)
    sale = Purchase.find(purchase_id)
    return if sale.is_recurring_subscription_charge
    # Cancellation posts are delivered on the subscription path, which rechecks the membership
    # is still ended. This path can't, so stale enqueued jobs bail out here.
    return if member_cancellation_trigger?

    sale = sale.original_purchase
    return unless sale.can_contact?
    return if sale.chargedback_not_reversed_or_refunded?
    return if sale.subscription.present? && !sale.subscription.alive?

    other_purchase_ids = Purchase.where(email: sale.email, seller_id: sale.seller_id)
                                 .all_success_states

View on GitHub (pinned to afeacbd394)

Solutions

  1. Retry after a short wait — provider errors here are typically transient; the request itself was well-formed.
  2. If it persists, check provider status pages and the app's Resend/SendGrid credentials/configuration (both providers are in play for any single preview).
  3. Verify the sender domain is still verified with whichever provider serves the recipient's domain (UNITED_INTERNET_RECIPIENT_DOMAINS decides).
  4. Keep rescuing Installment::PreviewEmailError → 422 with the message so users see 'try again later' rather than a 500.

Example fix

# before: unhandled provider error surfaced as 500
installment.send_preview_email(user)

# after: caller maps PreviewEmailError to a retryable 422 (pattern from PreviewEmailsController)
begin
  installment.send_preview_email(user)
rescue Installment::PreviewEmailError => e
  render json: { message: e.message }, status: :unprocessable_entity
end
Defensive patterns

Strategy: retry

Try / catch

begin
  installment.send_preview_email(user)
rescue Installment::PreviewEmailError => e
  render json: { message: e.message }, status: :unprocessable_entity # message says 'try again later' — safe to retry after a pause
end

Prevention

When it happens

Trigger: POST preview email for a non-abandoned-cart installment (the PostEmailApi.process branch) while the chosen provider — Resend or SendGrid, selected by recipient domain and random split — returns an API error: outage, throttling, invalid/revoked API key, or sender-domain verification failure.

Common situations: ESP rate limits during bulk preview testing; a rotated SendGrid/Resend key not propagated; provider incident; sandbox/staging env pointing at a provider with unverified sender domain.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/dc522e0a2b5ea446. Report an issue: GitHub.