antiwork/gumroad · error · Link::LinkInvalid

File(s) #{missing_ids.join(', ')} no longer exist; they may

Error message

File(s) #{missing_ids.join(', ')} no longer exist; they may have been deleted by a concurrent request. Retry with the current file list.

What it means

Link::LinkInvalid raised in Api::V2::LinksController#update during the file-sync step: the request's files array references existing file ids (ones that matched alive files before the request — client ids like cli-upload-* are creates), but after taking a lock on the product's alive files, some referenced ids are gone. That means a concurrent request deleted those files between this request's read and its write, so applying the payload would silently resurrect or 404; the API refuses and tells the client to refetch.

Source

Thrown at app/controllers/api/v2/links_controller.rb:487

            @product.custom_html = nil
            sanitization_report = Ai::PageSanitizer.empty_report
          else
            result = Ai::PageSanitizer.sanitize_with_report(params[:custom_html])
            @product.custom_html = result.html.presence
            sanitization_report = result.report
          end
        end

        flag_changed = @product.has_same_rich_content_for_all_variants? != rich_content_flag_was

        unless @normalized_files.nil?
          # Unknown client ids (cli-upload-*) are creates. Only ids that matched
          # an alive file before this request can be a concurrent delete.
          referenced_existing_ids = @normalized_files.filter_map { |f| f[:id] if f[:id].present? && @known_existing_file_ids&.include?(f[:id]) }
          if referenced_existing_ids.any?
            locked_alive_ids = @product.product_files.alive.lock.map(&:external_id)
            missing_ids = referenced_existing_ids - locked_alive_ids
            raise Link::LinkInvalid, "File(s) #{missing_ids.join(', ')} no longer exist; they may have been deleted by a concurrent request. Retry with the current file list." if missing_ids.any?
          end

          validate_file_embed_conflicts!(skip_variant_embeds: flag_changed && @product.has_same_rich_content_for_all_variants? && !@normalized_rich_content.nil?)

          rich_content_params = build_rich_content_params
          file_id_mappings = SaveFilesService.perform(@product, { files: @normalized_files }, rich_content_params) || {}
        end

        @product.save!

        @product.save_tags!(params[:tags]) if params.key?(:tags)

        if params.key?(:cover_ids)
          cover_ids = normalize_params_recursively(params[:cover_ids])
          @product.reorder_previews(cover_ids.map.with_index.to_h)
        end

        if !@normalized_rich_content.nil? && !@product.has_same_rich_content_for_all_variants? && @product.alive_variants.exists?

View on GitHub (pinned to afeacbd394)

Solutions

  1. Refetch the product's current file list (GET the link), merge your intended changes onto that fresh list, and resubmit.
  2. In integrations, serialize file mutations per product (one writer at a time) or refetch immediately before each write.
  3. If the deletion was unexpected, audit who deleted the files (concurrent request, another team's script) before retrying.
  4. Don't blindly strip the missing ids from the payload without refetching — other fields may also be stale.

Example fix

# before: read-modify-write across a long gap
files = LinkApi.get(link_id).files
# ... minutes later ...
LinkApi.update(link_id, files: files)
# after: refetch right before the write
fresh = LinkApi.get(link_id).files
merged = merge_changes(fresh, intended_changes)
LinkApi.update(link_id, files: merged)
Defensive patterns

Strategy: retry

Validate before calling

# refetch and diff immediately before the write
alive_ids = LinkApi.get(link_id).files.map { |f| f[:id] }
rejected = payload_files.map { |f| f[:id] }.compact - alive_ids - new_client_ids
refresh_list_then_merge if rejected.any?

Type guard

def referenced_files_alive?(payload_files, current_alive_ids)
  referenced = payload_files.filter_map { |f| f[:id] }
  referenced.empty? || (referenced - current_alive_ids).empty?
end

Try / catch

begin
  LinkApi.update(link_id, files: payload)
rescue Link::LinkInvalid => e
  if e.message.include?("no longer exist")
    payload = merge_onto(LinkApi.get(link_id).files, changes)
    retry
  else
    raise
  end
end

Prevention

When it happens

Trigger: PUT/PATCH /api/v2/links/:id with files containing ids of files that a concurrent request deleted after this request loaded @known_existing_file_ids — the locked alive scan (product_files.alive.lock) no longer finds them, producing missing_ids.

Common situations: Two editor sessions or an API integration deleting files while another update is in flight; a stale admin tab submitting an old file list after files were removed elsewhere; automation pipelines doing read-modify-write on the files array without refetching.

Related errors


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