antiwork/gumroad · error · Purchase::PurchaseInvalid
The bundle's contents have changed. Please refresh the page!
Error message
The bundle's contents have changed. Please refresh the page!
What it means
validate_bundle_products compares params[:bundle_products] against product.bundle_products.alive: every live bundle component must have a submitted row whose product_id (external), variant_id (external, nil allowed for variant-less components) and quantity all match. Any component without a matching row means the bundle was edited after checkout loaded, so the purchase is rejected with 'The bundle's contents have changed.'
Source
Thrown at app/services/purchase/create_service.rb:390
free_trial_params = params[:perceived_free_trial_duration]
if product.free_trial_enabled?
if !free_trial_params.present? || !free_trial_params[:amount].present? || !free_trial_params[:unit].present?
raise Purchase::PurchaseInvalid, "Invalid free trial information provided. Please try again."
elsif free_trial_params[:amount].to_i != product.free_trial_duration_amount || free_trial_params[:unit] != product.free_trial_duration_unit
raise Purchase::PurchaseInvalid, "The product's free trial has changed, please refresh the page!"
end
elsif free_trial_params.present?
raise Purchase::PurchaseInvalid, "Invalid free trial information provided. Please try again."
end
end
def validate_bundle_products
return unless product.is_bundle?
product.bundle_products.alive.each do |bundle_product|
if params[:bundle_products].none? { _1[:product_id] == bundle_product.product.external_id && _1[:variant_id] == bundle_product.variant&.external_id && _1[:quantity].to_i == bundle_product.quantity }
raise Purchase::PurchaseInvalid, "The bundle's contents have changed. Please refresh the page!"
end
validate_bundle_component_inventory(bundle_product)
end
end
# Validated under the bundle's product_inventory lock, before purchase.charge! — refusing here
# costs the buyer nothing. Only closes the non-concurrent hole (verdict computed then discarded);
# the multi-lock direct-vs-bundle/bundle-vs-bundle race is gumroad-private#1786 items 2-4.
def validate_bundle_component_inventory(bundle_product)
requested_quantity = bundle_product.quantity * purchase.quantity
component = bundle_product.product.reload
# A variant can have stock left while its product-wide cap is exhausted (or vice versa),
# so both must be checked — checking only one lets the other's cap be silently oversold.
remaining = if bundle_product.variant.present?
[bundle_product.variant.reload.quantity_left, component.remaining_for_sale_count].compact.min
elseView on GitHub (pinned to afeacbd394)
Solutions
- Refresh the checkout page to reload the bundle's current composition and resubmit.
- Integrators: rebuild the bundle_products array from product.bundle_products.alive at submit time rather than caching it.
- Handle this raise by restarting checkout - retrying identical params will keep failing.
Example fix
# before: cached bundle contents submitted after the seller edited the bundle
params[:bundle_products] = cached_bundle_rows
# after: rebuild rows from the product's current live composition
params[:bundle_products] = product.bundle_products.alive.map do |bp|
{ product_id: bp.product.external_id, variant_id: bp.variant&.external_id, quantity: bp.quantity }
end Defensive patterns
Strategy: validation
Validate before calling
if product.is_bundle?
params[:bundle_products] = product.bundle_products.alive.map do |bp|
{ product_id: bp.product.external_id, variant_id: bp.variant&.external_id, quantity: bp.quantity }
end
end Type guard
def bundle_params_current?(params, product)
product.bundle_products.alive.all? do |bp|
params[:bundle_products].any? { _1[:product_id] == bp.product.external_id && _1[:variant_id] == bp.variant&.external_id && _1[:quantity].to_i == bp.quantity }
end
end Try / catch
begin Purchase::CreateService.new(product:, params:, buyer:).perform rescue Purchase::PurchaseInvalid => e # bundle was edited mid-checkout: reload composition and restart checkout redirect_to_checkout if e.message == "The bundle's contents have changed. Please refresh the page!" end
Prevention
- Rebuild the bundle_products array from product.bundle_products.alive at submit time.
- Match on all three fields (product, variant, quantity) - a partial match still fails validation.
When it happens
Trigger: product.is_bundle? and some alive bundle_product has no entry in params[:bundle_products] where _1[:product_id] == bundle_product.product.external_id && _1[:variant_id] == bundle_product.variant&.external_id && _1[:quantity].to_i == bundle_product.quantity.
Common situations: Seller adds/removes bundle components or changes per-bundle quantity while buyers are in checkout; integrations caching the bundle composition; buyer's session holding a bundle snapshot from an earlier edit.
Related errors
- The cart does not have any products to which the upsell appl
- Invalid free trial information provided. Please try again.
- The product's free trial has changed, please refresh the pag
- #{component.name} is no longer available in the quantity thi
- The product's variants have changed, please refresh the page
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/c011f78a763c979d.
Report an issue: GitHub.