antiwork/gumroad · error · Link::LinkInvalid

File embeds reference files not belonging to this product: #

Error message

File embeds reference files not belonging to this product: #{missing_external.join(", ")}

What it means

Link::LinkInvalid raised in Api::V2::VariantsController#update right after saving variant rich content: the saved pages embed file ids (embedded_product_file_ids_in_order) whose count exceeds the alive product files actually found scoped to this product (@product.product_files.alive.where(id: file_ids)). The missing ids are files deleted, belonging to another product, or never created — embedding them would render dead file blocks, so the update is aborted inside the transaction and the ids are reported obfuscated (ObfuscateIds.encrypt).

Source

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

      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

        @product.reload
        @product.is_licensed = @product.has_embedded_license_key?
        @product.is_multiseat_license = false if !@product.is_licensed
        @product.content_updated_at = Time.current
        @product.save!

        @product.generate_product_files_archives!
      end
    end

View on GitHub (pinned to afeacbd394)

Solutions

  1. Replace or remove the reported embeds (the message lists them encrypted, matching API-visible ids) so every embedded file is an alive file of this product.
  2. Upload the missing files to this product first, then reference their ids in the embeds.
  3. When copying content between products, re-map file embeds to the destination product's files rather than copying ids verbatim.
  4. Verify id encoding: the API compares raw ids but reports them obfuscated — make sure your client sends the id form the upload/create response gave you.

Example fix

# before: embeds reference another product's file
VariantApi.update(link_id, variant_id, rich_content: pages_with_foreign_embed)
# after: upload to this product and re-point the embeds
upload = FileApi.upload(link_id, io)
pages = repoint_embeds(pages_with_foreign_embed, old_id: foreign_id, new_id: upload.id)
VariantApi.update(link_id, variant_id, rich_content: pages)
Defensive patterns

Strategy: validation

Validate before calling

file_ids = pages.flat_map { extract_embedded_file_ids(_1) }.uniq
alive = product.product_files.alive.where(id: file_ids).count
Result.invalid("embeds reference missing files") unless alive == file_ids.length

Type guard

def all_embeds_owned_by_product?(pages, product)
  ids = pages.flat_map { |p| extract_file_ids_from_nodes(p) }.uniq
  ids.empty? || product.product_files.alive.where(id: ids).count == ids.length
end

Prevention

When it happens

Trigger: PUT/PATCH variant with rich_content embedding file ids that are not alive files of the same product — e.g. ids copied from another product's content, ids of files deleted in a concurrent request, or raw external ids the client fabricated/misdecoded.

Common situations: Copy-pasting content blocks between products (embeds carry the source product's file ids); clients sending obfuscated ids where raw ids are expected (or vice versa); files removed after the client composed the content; template imports with hardcoded embeds.

Related errors


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