antiwork/gumroad · error · Link::LinkInvalid

The product needs to be shippable to at least one destinatio

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.

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)

Solutions

  1. Keep at least one destination row for published physical products — enforce '≥1 row' in the form before submit.
  2. 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.
  3. 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

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


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