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_statesView on GitHub (pinned to afeacbd394)
Solutions
- Retry after a short wait — provider errors here are typically transient; the request itself was well-formed.
- If it persists, check provider status pages and the app's Resend/SendGrid credentials/configuration (both providers are in play for any single preview).
- Verify the sender domain is still verified with whichever provider serves the recipient's domain (UNITED_INTERNET_RECIPIENT_DOMAINS decides).
- 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
- Rate-limit preview sends to avoid ESP throttling.
- Keep provider credentials (Resend and SendGrid) current — either may serve any given preview.
- Never let ResendApiResponseError/SendGridApiResponseError escape unhandled; the model normalizes both to PreviewEmailError by design.
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
- You have to confirm your email address before you can do tha
- typia.assert<{ error: string }>(json).error
- typia.assert<{ message: string }>(await response.json()).mes
- Sorry, something went wrong. Please try again.
- Gift purchases cannot be on installment plans.
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/dc522e0a2b5ea446.
Report an issue: GitHub.