antiwork/gumroad · error · Link::LinkInvalid
The product needs to be shippable to at least one…
Error message
The product needs to be shippable to at least one destination.
What it means
Raised as Link::LinkInvalid by Link#save_shipping_destinations! when a published (alive?) physical product submits an empty shipping_destinations array (or a single blank string). A published physical product must remain shippable to at least one destination, so clearing all destinations in one save is rejected after the uniqueness check.
Solutions
- Keep at least one destination row for published physical products — enforce '≥1 row' in the form before submit.
- If the seller truly wants no shipping, unpublish the product first (the guard only applies to alive? products) or convert it to a non-physical product.
- Rescue Link::LinkInvalid and surface 'The product needs to be shippable to at least one destination.'
Example fix
# before
link.save_shipping_destinations!([]) # LinkInvalid: published physical product
# after
if link.alive? && link.is_physical && destinations.blank?
return { error: "The product needs to be shippable to at least one destination." }
end
link.save_shipping_destinations!(destinations) Defensive patterns
Strategy: validation
Validate before calling
if link.alive? && link.is_physical && destinations.select { |d| d['country_code'].present? }.empty?
return { error: 'Published physical products need >=1 destination.' }
end Try / catch
begin
link.save_shipping_destinations!(destinations)
rescue Link::LinkInvalid
render json: { errors: link.errors.full_messages }, status: :unprocessable_entity
end Prevention
- Block deletion of the last destination row for published physical products.
- Offer unpublish-first flow when the seller wants to strip all destinations.
- Remember the guard keys on alive? — unpublished products may clear destinations.
When it happens
Trigger: Updating a published, alive physical product with shipping_destinations: [] or ['']. Unpublished/deleted products may clear destinations; the guard keys on alive?.
Common situations: Seller deletes every destination row in the shipping table and hits save while the product is live; front-end sends an empty array when the last row is removed; converting product content while forgetting the shipping requirement.
Related errors
- Sorry, shipping destinations have to be unique.
- Memberships should have at least one tier.
- Memberships should only have one Tier version category.
- You have to confirm your email address before you can do…
- Sorry, the price entered is too large.
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/3fbe93b0fbe5a53c.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/link.rb:958
[]
end
end
def save_shipping_destinations!(shipping_destinations)
shipping_destinations ||= []
deduped_destinations = shipping_destinations.uniq { |destination| destination["country_code"] }
if deduped_destinations.size != shipping_destinations.size
errors.add(:base, "Sorry, shipping destinations have to be unique.")
raise LinkInvalid, "Sorry, shipping destinations have to be unique."
end
remaining_shipping_destinations = self.shipping_destinations.alive.pluck(:id)
# Cannot empty out shipping destinations for a published physical product
if alive? && (shipping_destinations.empty? || shipping_destinations.first == "")
errors.add(:base, "The product needs to be shippable to at least one destination.")
raise LinkInvalid, "The product needs to be shippable to at least one destination."
end
shipping_destinations.each do |destination|
next if destination.try(:[], "country_code").blank?
shipping_destination = ShippingDestination.find_or_create_by(country_code: destination["country_code"], link_id: id)
# TODO: :product_edit_react cleanup
one_item_rate_cents = destination["one_item_rate_cents"]
multiple_items_rate_cents = destination["multiple_items_rate_cents"]
one_item_rate_cents ||= string_to_price_cents(price_currency_type, destination["one_item_rate"])
multiple_items_rate_cents ||= string_to_price_cents(price_currency_type, destination["multiple_items_rate"])
begin
shipping_destination.one_item_rate_cents = one_item_rate_cents
shipping_destination.multiple_items_rate_cents = multiple_items_rate_cents
shipping_destination.deleted_at = nil
shipping_destination.is_virtual_country = ShippingDestination::Destinations::VIRTUAL_COUNTRY_CODES.include?(shipping_destination.country_code)
View on GitHub (pinned to afeacbd394)