antiwork/gumroad · error · Link::LinkInvalid

Sorry, shipping destinations have to be unique.

Error message

Sorry, shipping destinations have to be unique.

What it means

Raised as Link::LinkInvalid (app/models/link.rb:1702) by Link#save_shipping_destinations! when the submitted destination list contains two entries with the same country_code (uniq by country_code shrinks the list). One shipping rate row per country per product is the data model's invariant, so duplicates are rejected before any ShippingDestination find_or_create runs.

Source

Thrown at app/models/link.rb:950

    end
  end

  def removed_file_info_attributes
    removed_file_info_attributes = self.json_data.present? ? self.json_data["removed_file_info_attributes"] : []
    if removed_file_info_attributes.present?
      removed_file_info_attributes.map(&:to_sym)
    else
      []
    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"])

View on GitHub (pinned to afeacbd394)

Solutions

  1. Dedupe by country_code client-side before submit — replace the existing row for that country instead of adding a second.
  2. Prevent selecting an already-chosen country in the destination picker.
  3. Server-side callers can shipping_destinations.uniq { |d| d['country_code'] } before invoking save_shipping_destinations! when last-write-wins is acceptable.
  4. Rescue Link::LinkInvalid and show errors.full_messages.

Example fix

# before
link.save_shipping_destinations!(params[:shipping_destinations]) # LinkInvalid: duplicates

# after
save_shipping_destinations!(params[:shipping_destinations])
# where the controller/serializer first normalizes:
normalized = raw_destinations.index_by { |d| d['country_code'] }.values # last row per country wins
link.save_shipping_destinations!(normalized)
Defensive patterns

Strategy: validation

Validate before calling

deduped = shipping_destinations.uniq { |d| d['country_code'] }
raise ArgumentError, 'duplicate country' if deduped.size != shipping_destinations.size

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: POST/PATCH product params whose shipping_destinations array includes e.g. two {'country_code' => 'US'} entries — typically a UI bug appending an edited row instead of replacing it, or a client merging destination lists.

Common situations: React/jQuery shipping table letting the seller pick the same country twice; spreadsheet/CSV import concatenating rows; duplicate form submission re-appending the row.

Related errors


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