{"record":{"id":"fd3f92948705d1fd","repo":"antiwork/gumroad","slug":"file-embeds-reference-files-not-belonging-to-this","errorCode":null,"errorMessage":"File embeds reference files not belonging to this product: #{missing_external.join(\", \")}","messagePattern":"File embeds reference files not belonging to this product: #(.+?)","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":200,"severity":"error","filePath":"app/controllers/api/v2/variants_controller.rb","lineNumber":48,"sourceCode":"      if !@variant.update(permitted_params)\n        raise ActiveRecord::Rollback\n      end\n\n      if params.key?(:rich_content)\n        @product.lock!\n        if @product.has_same_rich_content_for_all_variants?\n          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.\"\n        end\n\n        save_variant_rich_content!(@variant)\n\n        file_ids = @variant.alive_rich_contents.flat_map { _1.embedded_product_file_ids_in_order }.uniq\n        if file_ids.any?\n          scoped_files = @product.product_files.alive.where(id: file_ids).to_a\n          if scoped_files.length != file_ids.length\n            missing = file_ids - scoped_files.map(&:id)\n            missing_external = missing.map { ObfuscateIds.encrypt(_1) }\n            raise Link::LinkInvalid, \"File embeds reference files not belonging to this product: #{missing_external.join(\", \")}\"\n          end\n          @variant.product_files = scoped_files\n        else\n          @variant.product_files = []\n        end\n\n        Product::SavePostPurchaseCustomFieldsService.new(@product).perform\n\n        @product.reload\n        @product.is_licensed = @product.has_embedded_license_key?\n        @product.is_multiseat_license = false if !@product.is_licensed\n        @product.content_updated_at = Time.current\n        @product.save!\n\n        @product.generate_product_files_archives!\n      end\n    end\n","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/api/v2/variants_controller.rb#L30-L66","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Upload the missing files to this product first, then reference their ids in the embeds.","When copying content between products, re-map file embeds to the destination product's files rather than copying ids verbatim.","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."],"exampleFix":"# before: embeds reference another product's file\nVariantApi.update(link_id, variant_id, rich_content: pages_with_foreign_embed)\n# after: upload to this product and re-point the embeds\nupload = FileApi.upload(link_id, io)\npages = repoint_embeds(pages_with_foreign_embed, old_id: foreign_id, new_id: upload.id)\nVariantApi.update(link_id, variant_id, rich_content: pages)","handlingStrategy":"validation","validationCode":"file_ids = pages.flat_map { extract_embedded_file_ids(_1) }.uniq\nalive = product.product_files.alive.where(id: file_ids).count\nResult.invalid(\"embeds reference missing files\") unless alive == file_ids.length","typeGuard":"def all_embeds_owned_by_product?(pages, product)\n  ids = pages.flat_map { |p| extract_file_ids_from_nodes(p) }.uniq\n  ids.empty? || product.product_files.alive.where(id: ids).count == ids.length\nend","tryCatchPattern":null,"preventionTips":["Only embed file ids returned by this product's upload/create responses.","Re-map embeds when copying content between products.","Re-check embeds against alive files right before submit; stale ids from deleted files fail the guard."],"tags":["api","variants","rich-content","files","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"}