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
- Refetch the product's current file list (GET the link), merge your intended changes onto that fresh list, and resubmit.
- In integrations, serialize file mutations per product (one writer at a time) or refetch immediately before each write.
- If the deletion was unexpected, audit who deleted the files (concurrent request, another team's script) before retrying.
- 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
- Do read-modify-write on the files array in one tight sequence; refetch right before update.
- Single-writer per product for file mutations in integrations.
- Treat this error as a signal to refresh all product state, not just the file ids.
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
- stale_content_conflict
- Cannot update product-level rich content while in per-varian
- Cannot remove files still referenced in rich content: #{conf
- Cannot switch to shared content: both product-level and vari
- Cannot switch to shared content: multiple variants have dist
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/b2d15e28b9ec8940.
Report an issue: GitHub.