{"record":{"id":"386b7c9a156d4012","repo":"antiwork/gumroad","slug":"could-not-disconnect-the-integration-name-tr","errorCode":null,"errorMessage":"Could not disconnect the #{integration.name.tr(\"_\", \" \")} integration, please try again.","messagePattern":"Could not disconnect the #(.+?) integration, please try again\\.","errorType":"validation","errorClass":"Link::LinkInvalid","httpStatus":null,"severity":"error","filePath":"app/services/product/save_integrations_service.rb","lineNumber":71,"sourceCode":"\n    other_products_by_user = Link.where(user_id: product.user_id).alive.where.not(id: product.id).pluck(:id)\n    integrations_on_other_products = Integration.joins(:product_integration).where(\"product_integration.product_id\" => other_products_by_user, \"product_integration.deleted_at\" => nil)\n\n    deleted_integrations = integrations_to_delete(enabled_integrations)\n    deletion_successful = product.live_product_integrations.where(integration: deleted_integrations).reduce(true) do |success, product_integration|\n      integration = product_integration.integration\n      same_connection_exists = integrations_on_other_products.find { |other_integration| integration.same_connection?(other_integration) }\n      disconnection_successful = same_connection_exists ? true : integration.disconnect!\n\n      if disconnection_successful\n        product_integration.mark_deleted\n        success\n      else\n        product.errors.add(:base, \"Could not disconnect the #{integration.name.tr(\"_\", \" \")} integration, please try again.\")\n        false\n      end\n    end\n    raise Link::LinkInvalid unless deletion_successful\n\n    product.active_integrations << enabled_integrations - product.active_integrations\n  end\n\n  private\n    def contract_enforced?\n      contract.present? && contract.enforced?\n    end\n\n    # True when this request expressed no intent about integrations at all:\n    # the collection wasn't submitted (absent and {} read the same, Rule 1)\n    # and no explicit deletion targets it. `submitted?` is presence-based, so\n    # an empty hash — the shape strong parameters produces when every entry\n    # is malformed — also counts as \"not submitted\".\n    def no_integrations_intent?\n      !contract.submitted?(:integrations) &&\n        contract.deleted_ids(:integrations).empty? &&\n        !contract.cleared?(:integrations)","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/product/save_integrations_service.rb#L53-L89","documentation":"Product::SaveIntegrationsService diffs the product's active integrations against the submitted ones; for each removal whose connection is not reused on another of the seller's products, it calls integration.disconnect! — a live, irreversible third-party call (Discord bot removal, Google OAuth token revocation). If disconnect! returns falsy, the error is added to product.errors and Link::LinkInvalid is raised, so the product save fails rather than half-commit.","triggerScenarios":"Removing a Discord/Google/Circle integration from a product when the provider API call inside disconnect! errors or returns false: expired or already-revoked OAuth tokens, provider outage, rate limiting, or network failure to the third party.","commonSituations":"Revoking an integration whose credentials expired long ago (the revoke call itself fails); provider-side incidents; flaky egress networking.","solutions":["Retry the save — transient provider errors usually clear on the next attempt","If it keeps failing, reconnect/re-authenticate the integration first, then remove it","Check the provider's status page (Discord/Google/etc.) and the app logs for the underlying disconnect! failure","Removing the same connection from a product while another product still uses it skips disconnect! entirely — so as a workaround, the connection on other products is unaffected either way"],"exampleFix":"# before\nProduct::SaveIntegrationsService.perform(product, { discord: nil })\n# provider 500 -> product.errors[:base], Link::LinkInvalid\n\n# after\nbegin\n  Product::SaveIntegrationsService.perform(product, { discord: nil })\nrescue Link::LinkInvalid\n  retry if (attempts += 1) < 3 # transient provider failure\n  raise\nend","handlingStrategy":"retry","validationCode":"# Best available pre-check: verify the connection is still live before removing\nintegration = product.find_integration_by_name(name)\nraise 'integration not connected' if integration.nil?\n# disconnect! hits the provider; a dead/expired token is the usual persistent failure","typeGuard":null,"tryCatchPattern":"begin\n  attempts = (attempts || 0) + 1\n  Product::SaveIntegrationsService.perform(product, integration_params)\nrescue Link::LinkInvalid\n  if product.errors[:base].any? { _1.start_with?('Could not disconnect') } && attempts < 3\n    sleep(2**attempts) && retry # transient provider failure\n  else\n    raise # persistent: re-authenticate the integration, then remove again\n  end\nend","preventionTips":["disconnect! is a live third-party call with no undo — only include integration removals in saves that truly intend it","Re-authenticate long-expired integrations before removing them; revoking dead tokens is the classic persistent failure","Remember the same connection used by another product skips disconnect! entirely, so cross-product reuse dodges the provider call","Check the provider's status page before retrying during an incident"],"tags":["ruby","rails","integrations","third-party-api","product-save"],"backgroundTag":"third-party-api-failure","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}