antiwork/gumroad · error · Link::LinkInvalid

Cannot update variant rich content while the product uses sh

Error message

Cannot update variant rich content while the product uses shared content for all variants. Update product-level rich content instead, or set has_same_rich_content_for_all_variants to false first.

What it means

Link::LinkInvalid raised in Api::V2::VariantsController#update inside the transaction, after @product.lock!, when the request includes rich_content but the product still has has_same_rich_content_for_all_variants true. In shared mode variant-level content is not authoritative — writes must go to the product-level endpoint, or the flag must be flipped false first, otherwise the variant content would be shadowed by the shared set.

Source

Thrown at app/controllers/api/v2/variants_controller.rb:37

    else
      error_with_creating_object(:variant, variant)
    end
  end

  def show
    success_with_variant(@variant)
  end

  def update
    ActiveRecord::Base.transaction do
      if !@variant.update(permitted_params)
        raise ActiveRecord::Rollback
      end

      if params.key?(:rich_content)
        @product.lock!
        if @product.has_same_rich_content_for_all_variants?
          raise Link::LinkInvalid, "Cannot update variant rich content while the product uses shared content for all variants. Update product-level rich content instead, or set has_same_rich_content_for_all_variants to false first."
        end

        save_variant_rich_content!(@variant)

        file_ids = @variant.alive_rich_contents.flat_map { _1.embedded_product_file_ids_in_order }.uniq
        if file_ids.any?
          scoped_files = @product.product_files.alive.where(id: file_ids).to_a
          if scoped_files.length != file_ids.length
            missing = file_ids - scoped_files.map(&:id)
            missing_external = missing.map { ObfuscateIds.encrypt(_1) }
            raise Link::LinkInvalid, "File embeds reference files not belonging to this product: #{missing_external.join(", ")}"
          end
          @variant.product_files = scoped_files
        else
          @variant.product_files = []
        end

        Product::SavePostPurchaseCustomFieldsService.new(@product).perform

View on GitHub (pinned to afeacbd394)

Solutions

  1. Update product-level rich content instead (links endpoint) — in shared mode that is the single source the variants render.
  2. Or send has_same_rich_content_for_all_variants: false on the product first, then write variant content (mind the migrate_to_per_variant constraints).
  3. In clients, branch on the product's flag before choosing the variant vs product endpoint.
  4. Refetch the product if the mode may have changed since your last read — the guard re-checks under lock.

Example fix

# before: variant rich_content on a shared-content product
VariantApi.update(link_id, variant_id, rich_content: pages)  # -> LinkInvalid
# after: flip the product to per-variant mode first
LinkApi.update(link_id, has_same_rich_content_for_all_variants: false)
VariantApi.update(link_id, variant_id, rich_content: pages)
Defensive patterns

Strategy: validation

Validate before calling

product = LinkApi.get(link_id)
raise ModeError if params.key?(:rich_content) && product.has_same_rich_content_for_all_variants?

Type guard

def variant_rich_content_writable?(product)
  !product.has_same_rich_content_for_all_variants? || product.alive_variants.empty?
end

Prevention

When it happens

Trigger: PUT/PATCH /api/v2/links/:link_id/variants/:id (or nested equivalent) with a rich_content key while the parent product uses shared content for all variants; the guard fires before save_variant_rich_content! so nothing is written.

Common situations: Integrations that always write per-variant content regardless of product mode; UI per-variant editor reachable on a shared-content product; state changed by another client between page load and submit.

Related errors


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