antiwork/gumroad · error · Purchase::PurchaseInvalid

The product's variants have changed, please refresh the page

Error message

The product's variants have changed, please refresh the page!

What it means

Each external_id in params[:variants] is looked up in product.current_base_variants during build_purchase; an id that no longer resolves means the variant was deleted, archived, or replaced since checkout loaded. The message is added to purchase.errors and raised so bundle-checkout error rendering has a purchase object to attach it to.

Source

Thrown at app/services/purchase/create_service.rb:453

      purchase.offer_code = product.find_offer_code(code: purchase.discount_code.downcase.strip) if purchase.discount_code.present?
      if purchase.offer_code.present? && product.default_offer_code.present? && purchase.offer_code.id == product.default_offer_code.id
        purchase.default_offer_code_id = purchase.offer_code.id
      end
      purchase.business_vat_id = (params_for_purchase[:business_vat_id] && params_for_purchase[:business_vat_id].size > 0 ? params_for_purchase[:business_vat_id] : nil)
      purchase.is_original_subscription_purchase = (product.is_recurring_billing && !params_for_purchase[:is_gift_receiver_purchase]) || purchase.is_installment_payment
      purchase.is_free_trial_purchase = product.free_trial_enabled? && !is_gift?
      purchase.should_exclude_product_review = product.free_trial_enabled? && !is_gift?

      Shipment.create(purchase:) if should_ship

      if params[:variants].present?
        params[:variants].each do |external_id|
          variant = product.current_base_variants.find_by_external_id(external_id)
          if variant.present?
            purchase.variant_attributes << variant
          else
            purchase.errors.add(:base, "The product's variants have changed, please refresh the page!")
            raise Purchase::PurchaseInvalid, "The product's variants have changed, please refresh the page!"
          end
        end
      elsif product.is_tiered_membership
        purchase.variant_attributes << product.tiers.first
      elsif product.is_physical && product.skus.is_default_sku.present?
        purchase.variant_attributes << product.skus.is_default_sku.first
      end

      if product.native_type == Link::NATIVE_TYPE_CALL
        start_time = Time.zone.parse(params[:call_start_time] || "")
        duration_in_minutes = purchase.variant_attributes.first&.duration_in_minutes

        if start_time.blank? || duration_in_minutes.blank?
          raise Purchase::PurchaseInvalid, "Please select a start time."
        end

        end_time = start_time + duration_in_minutes.minutes
        purchase.build_call(start_time:, end_time:)

View on GitHub (pinned to afeacbd394)

Solutions

  1. Refresh the checkout page and re-select a current variant.
  2. Integrators: re-fetch the product's current_base_variants at submit time and drop ids that no longer resolve.
  3. On this raise, reload the variant list and restart checkout rather than retrying the same params.

Example fix

# before: stale variant id persisted from an earlier page state
params[:variants] = ['var_stale123']

# after: keep only ids that still resolve against the product
params[:variants] = params[:variants] & product.current_base_variants.pluck(:external_id)
Defensive patterns

Strategy: validation

Validate before calling

current_ids = product.current_base_variants.pluck(:external_id)
params[:variants] = params[:variants]&.select { current_ids.include?(_1) }

Type guard

def variants_current?(variant_ids, product)
  (variant_ids.to_a - product.current_base_variants.pluck(:external_id)).empty?
end

Try / catch

begin
  Purchase::CreateService.new(product:, params:, buyer:).perform
rescue Purchase::PurchaseInvalid => e
  # variant set changed: reload variants, force re-selection, restart checkout
  redirect_to_checkout if e.message == "The product's variants have changed, please refresh the page!"
end

Prevention

When it happens

Trigger: params[:variants] contains at least one external_id for which product.current_base_variants.find_by_external_id returns nil.

Common situations: Seller edits or removes variants mid-checkout; client persisting a stale variant selection; integrations caching variant ids across product edits.

Related errors


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