{"record":{"id":"f42b785fa843c4c3","repo":"antiwork/gumroad","slug":"cannot-remove-files-still-referenced-in-rich-conte","errorCode":null,"errorMessage":"Cannot remove files still referenced in rich content: #{conflicting.join(\", \")}. Remove the file embeds from rich content first, or send both changes together.","messagePattern":"Cannot remove files still referenced in rich content: #(.+?)\\. Remove the file embeds from rich content first, or send both changes together\\.","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":200,"severity":"error","filePath":"app/controllers/api/v2/links_controller.rb","lineNumber":907,"sourceCode":"      return if removing_ids.empty?\n\n      product_embed_ids = if @normalized_rich_content\n        extract_file_embed_ids_from_params(@normalized_rich_content)\n      else\n        @product.alive_rich_contents.flat_map(&:embedded_product_file_ids_in_order).map { ObfuscateIds.encrypt(_1) }\n      end\n\n      variant_embed_ids = if skip_variant_embeds\n        []\n      else\n        @product.alive_variants.flat_map { |v| v.alive_rich_contents.flat_map(&:embedded_product_file_ids_in_order) }.map { ObfuscateIds.encrypt(_1) }\n      end\n\n      all_embed_ids = (product_embed_ids + variant_embed_ids).uniq\n      conflicting = removing_ids & all_embed_ids\n      return if conflicting.empty?\n\n      raise Link::LinkInvalid, \"Cannot remove files still referenced in rich content: #{conflicting.join(\", \")}. Remove the file embeds from rich content first, or send both changes together.\"\n    end\n\n    def extract_file_embed_ids_from_params(rich_content_pages)\n      return [] if rich_content_pages.blank?\n\n      rich_content_pages.flat_map do |page|\n        content = unwrap_description_content(page[:description])\n        next [] if content.blank?\n        extract_file_ids_from_nodes(content)\n      end.compact.uniq\n    end\n\n    def extract_file_ids_from_nodes(nodes)\n      nodes.flat_map do |node|\n        ids = []\n        if node[:type] == RichContent::FILE_EMBED_NODE_TYPE\n          id = node.dig(:attrs, :id)\n          ids << id if id.present?","sourceCodeStart":889,"sourceCodeEnd":925,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/api/v2/links_controller.rb#L889-L925","documentation":"Link::LinkInvalid raised by validate_file_embed_conflicts! in Api::V2::LinksController#update: the request removes files (ids in removing_ids) that are still embedded in product-level or variant-level rich content (embedded_product_file_ids_in_order, obfuscated for comparison). Deleting an embedded file would leave dead embeds in the content, so the API requires you to remove the embeds first or send both changes atomically.","triggerScenarios":"PATCH /api/v2/links/:id whose files array drops a file while current rich content (product pages, or variant pages unless skip_variant_embeds) still references that file's id; e.g. deleting a PDF that a content page embeds.","commonSituations":"Integrations managing the file list without parsing rich content for embeds; sellers deleting files from the file manager while the content still shows them; UI file-delete not offering to also strip the embed.","solutions":["Remove the file embeds from rich_content first (or in the same request), then delete the files — the message names the conflicting ids.","When building clients, before removing a file, scan current rich content for embeds of that id and rewrite those nodes in the same payload.","If you intend to keep the embeds, keep the files alive (don't send them in the removal set).","Note the skip_variant_embeds subtlety: a flag change request relaxes variant checks in one path — revalidate after the write rather than assuming."],"exampleFix":"# before: delete a file that a page still embeds\nLinkApi.update(link_id, files: current_files.reject { |f| f[:id] == target_id })\n# after: strip embeds from the content in the same request\ncleaned_pages = remove_embeds(pages, target_id)\nLinkApi.update(link_id, rich_content: cleaned_pages, files: current_files.reject { |f| f[:id] == target_id })","handlingStrategy":"validation","validationCode":"embeds = extract_file_embed_ids(rich_content_pages)\nconflicts = files_to_remove & embeds\nscrub_embeds!(rich_content_pages, conflicts) if conflicts.any? # then submit both together","typeGuard":"def removing_files_embedded?(removing_ids, product)\n  embed_ids = product.alive_rich_contents.flat_map(&:embedded_product_file_ids_in_order)\n  (removing_ids.map(&:to_i) & embed_ids).any?\nend","tryCatchPattern":null,"preventionTips":["Scan current rich content for embeds before removing any file; rewrite those nodes in the same payload.","Have the file-delete UI offer to strip the embeds too.","Remember variant content embeds count as well as product-level ones."],"tags":["api","files","rich-content","embeds","referential-integrity"],"backgroundTag":"referential-integrity-violation","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}