{"record":{"id":"dc522e0a2b5ea446","repo":"antiwork/gumroad","slug":"failed-to-send-preview-email-please-try-again-lat","errorCode":null,"errorMessage":"Failed to send preview email. Please try again later.","messagePattern":"Failed to send preview email\\. Please try again later\\.","errorType":"exception","errorClass":"Installment::PreviewEmailError","httpStatus":422,"severity":"error","filePath":"app/models/installment.rb","lineNumber":421,"sourceCode":"  end\n\n  def send_preview_email(recipient_user)\n    if recipient_user.has_unconfirmed_email?\n      raise PreviewEmailError, \"You have to confirm your email address before you can do that.\"\n    elsif abandoned_cart_type?\n      CustomerMailer.abandoned_cart_preview(recipient_user.id, id).deliver_later\n    else\n      recipient = { email: recipient_user.email }\n      recipient[:url_redirect] = UrlRedirect.find_or_create_by!(installment: self, purchase: nil) if has_files?\n      begin\n        PostEmailApi.process(post: self, recipients: [recipient], preview: true)\n      rescue ResendApiResponseError, SendGridApiResponseError\n        # Either provider can be the one that actually sends this preview: the\n        # recipient's domain decides (see\n        # MailerInfo::UNITED_INTERNET_RECIPIENT_DOMAINS), as does the random\n        # split, so a failure from either has to become the same friendly error\n        # rather than an unhandled 500 on the preview request.\n        raise PreviewEmailError, \"Failed to send preview email. Please try again later.\"\n      end\n    end\n  end\n\n  def send_installment_from_workflow_for_purchase(purchase_id, reschedule_reference_time: nil)\n    sale = Purchase.find(purchase_id)\n    return if sale.is_recurring_subscription_charge\n    # Cancellation posts are delivered on the subscription path, which rechecks the membership\n    # is still ended. This path can't, so stale enqueued jobs bail out here.\n    return if member_cancellation_trigger?\n\n    sale = sale.original_purchase\n    return unless sale.can_contact?\n    return if sale.chargedback_not_reversed_or_refunded?\n    return if sale.subscription.present? && !sale.subscription.alive?\n\n    other_purchase_ids = Purchase.where(email: sale.email, seller_id: sale.seller_id)\n                                 .all_success_states","sourceCodeStart":403,"sourceCodeEnd":439,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/models/installment.rb#L403-L439","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before: unhandled provider error surfaced as 500\ninstallment.send_preview_email(user)\n\n# after: caller maps PreviewEmailError to a retryable 422 (pattern from PreviewEmailsController)\nbegin\n  installment.send_preview_email(user)\nrescue Installment::PreviewEmailError => e\n  render json: { message: e.message }, status: :unprocessable_entity\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  installment.send_preview_email(user)\nrescue Installment::PreviewEmailError => e\n  render json: { message: e.message }, status: :unprocessable_entity # message says 'try again later' — safe to retry after a pause\nend","preventionTips":["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."],"tags":["installments","emails","preview","resend","sendgrid","esp","transient"],"backgroundTag":"email-provider-api-error","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}