{"record":{"id":"70e78f44a0bde19b","repo":"antiwork/gumroad","slug":"cannot-update-variant-rich-content-while-the-produ","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":200,"severity":"error","filePath":"app/controllers/api/v2/variants_controller.rb","lineNumber":37,"sourceCode":"    else\n      error_with_creating_object(:variant, variant)\n    end\n  end\n\n  def show\n    success_with_variant(@variant)\n  end\n\n  def update\n    ActiveRecord::Base.transaction do\n      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","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/api/v2/variants_controller.rb#L19-L55","documentation":"Link::LinkInvalid raised in Api::V2::VariantsController#update inside the transaction, after @product.lock!, when the request includes rich_content but the product still has has_same_rich_content_for_all_variants true. In shared mode variant-level content is not authoritative — writes must go to the product-level endpoint, or the flag must be flipped false first, otherwise the variant content would be shadowed by the shared set.","triggerScenarios":"PUT/PATCH /api/v2/links/:link_id/variants/:id (or nested equivalent) with a rich_content key while the parent product uses shared content for all variants; the guard fires before save_variant_rich_content! so nothing is written.","commonSituations":"Integrations that always write per-variant content regardless of product mode; UI per-variant editor reachable on a shared-content product; state changed by another client between page load and submit.","solutions":["Update product-level rich content instead (links endpoint) — in shared mode that is the single source the variants render.","Or send has_same_rich_content_for_all_variants: false on the product first, then write variant content (mind the migrate_to_per_variant constraints).","In clients, branch on the product's flag before choosing the variant vs product endpoint.","Refetch the product if the mode may have changed since your last read — the guard re-checks under lock."],"exampleFix":"# before: variant rich_content on a shared-content product\nVariantApi.update(link_id, variant_id, rich_content: pages)  # -> LinkInvalid\n# after: flip the product to per-variant mode first\nLinkApi.update(link_id, has_same_rich_content_for_all_variants: false)\nVariantApi.update(link_id, variant_id, rich_content: pages)","handlingStrategy":"validation","validationCode":"product = LinkApi.get(link_id)\nraise ModeError if params.key?(:rich_content) && product.has_same_rich_content_for_all_variants?","typeGuard":"def variant_rich_content_writable?(product)\n  !product.has_same_rich_content_for_all_variants? || product.alive_variants.empty?\nend","tryCatchPattern":null,"preventionTips":["Check the product's shared-content flag before calling the variant rich-content endpoint.","Flip the flag on the product first when per-variant content is intended.","Refetch product state at request time in multi-client setups."],"tags":["api","variants","rich-content","products","validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}