{"record":{"id":"abfac29f80ec1381","repo":"antiwork/gumroad","slug":"cannot-switch-to-shared-content-both-product-leve","errorCode":null,"errorMessage":"Cannot switch to shared content: both product-level and variant-level content exist. Remove one side first, or send replacement rich_content in the same request.","messagePattern":"Cannot switch to shared content: both product-level and variant-level content exist\\. Remove one side first, or send replacement rich_content in the same request\\.","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":200,"severity":"error","filePath":"app/controllers/api/v2/links_controller.rb","lineNumber":974,"sourceCode":"\n      removed = existing_rich_contents - rich_contents_to_keep\n      retire_upsells_from_rich_contents!(removed)\n      removed.each(&:mark_deleted!)\n    end\n\n    def migrate_rich_content_for_flag_change!\n      if @product.has_same_rich_content_for_all_variants?\n        migrate_to_shared_rich_content!\n      else\n        migrate_to_per_variant_rich_content!\n      end\n    end\n\n    def migrate_to_shared_rich_content!\n      variants_with_content = @product.alive_variants.select { |v| v.alive_rich_contents.any? }\n\n      if @product.alive_rich_contents.any? && variants_with_content.any?\n        raise Link::LinkInvalid, \"Cannot switch to shared content: both product-level and variant-level content exist. Remove one side first, or send replacement rich_content in the same request.\"\n      end\n\n      if variants_with_content.length > 1\n        canonical = canonicalize_rich_contents(variants_with_content.first)\n        all_identical = variants_with_content.all? { |v| canonicalize_rich_contents(v) == canonical }\n        if !all_identical\n          raise Link::LinkInvalid, \"Cannot switch to shared content: multiple variants have distinct content. Remove variant content first, or send replacement rich_content in the same request.\"\n        end\n      end\n\n      source_variant = nil\n      if @product.alive_rich_contents.empty? && variants_with_content.any?\n        source_variant = variants_with_content.first\n        source_variant.alive_rich_contents.sort_by(&:position).each_with_index do |rc, index|\n          @product.alive_rich_contents.create!(title: rc.title, description: rc.description_without_stale_dead_cross_product_file_embeds, position: index)\n        end\n      end\n","sourceCodeStart":956,"sourceCodeEnd":992,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/api/v2/links_controller.rb#L956-L992","documentation":"Link::LinkInvalid raised by migrate_to_shared_rich_content! when the product flips to shared content (has_same_rich_content_for_all_variants -> true) but BOTH product-level alive rich contents and at least one variant with alive rich contents exist. There is no unambiguous single source for the shared content, so the migration refuses instead of picking a winner silently; you must remove one side or send replacement rich_content with the switch.","triggerScenarios":"PATCH /api/v2/links/:id with has_same_rich_content_for_all_variants changing to true (and no replacement rich_content) on a product where @product.alive_rich_contents.any? && variants_with_content.any? — legacy mixed-state products hit this most.","commonSituations":"Products left in a mixed state by older flows or partial migrations; sellers who authored variant content while product-level content also exists; scripts toggling the flag without first reconciling content.","solutions":["Delete the variant-level content (or the product-level pages) first, then switch the flag.","Or send rich_content in the same request — replacement content wins and the both-sides ambiguity disappears.","For bulk migrations, pre-scan products for the mixed state and reconcile each before flipping flags.","Don't try to dodge it via raw DB updates — the same invariant is what the migration protects."],"exampleFix":"# before: ambiguous switch\nLinkApi.update(link_id, has_same_rich_content_for_all_variants: true)\n# after: send replacement shared content in the same request\nLinkApi.update(link_id, has_same_rich_content_for_all_variants: true, rich_content: new_shared_pages)","handlingStrategy":"validation","validationCode":"product = LinkApi.get(link_id)\nboth_sides = product.rich_contents.any? && product.variants.any? { |v| v.rich_contents.any? }\nsend_replacement_content! if both_sides # or delete one side first","typeGuard":"def safe_to_switch_to_shared?(product)\n  variants_with_content = product.alive_variants.select { |v| v.alive_rich_contents.any? }\n  !(product.alive_rich_contents.any? && variants_with_content.any?)\nend","tryCatchPattern":null,"preventionTips":["Reconcile mixed-content products before flipping the shared flag.","Send replacement rich_content with the switch to remove ambiguity.","Pre-scan products in bulk migrations for the both-sides state."],"tags":["api","rich-content","variants","migration","products","validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}