{"record":{"id":"518b791442b299dc","repo":"langgenius/dify","slug":"you-can-t-uninstall-an-app-owned-by-the-current-te","errorCode":null,"errorMessage":"You can't uninstall an app owned by the current tenant","messagePattern":"You can't uninstall an app owned by the current tenant","errorType":"http","errorClass":"BadRequest","httpStatus":400,"severity":"error","filePath":"api/controllers/console/explore/installed_app.py","lineNumber":275,"sourceCode":"        if current_user.current_tenant is None:\n            raise ValueError(\"current_user.current_tenant must not be None\")\n\n        current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant, session=db.session())\n        return dump_response(\n            InstalledAppResponse,\n            _installed_app_response_data(\n                installed_app,\n                app_model,\n                current_tenant_id=current_tenant_id,\n                current_user=current_user,\n            ),\n        )\n\n    @console_ns.response(204, \"App uninstalled successfully\")\n    @with_current_tenant_id\n    def delete(self, current_tenant_id: str, installed_app: InstalledApp):\n        if installed_app.app_owner_tenant_id == current_tenant_id:\n            raise BadRequest(\"You can't uninstall an app owned by the current tenant\")\n\n        db.session.delete(installed_app)\n        db.session.commit()\n\n        return \"\", 204\n\n    @console_ns.response(200, \"Success\", console_ns.models[SimpleResultMessageResponse.__name__])\n    @console_ns.expect(console_ns.models[InstalledAppUpdatePayload.__name__])\n    @model_validate(InstalledAppUpdatePayload)\n    def patch(self, req_data: InstalledAppUpdatePayload, installed_app: InstalledApp):\n        commit_args = False\n        if req_data.is_pinned is not None:\n            installed_app.is_pinned = req_data.is_pinned\n            commit_args = True\n\n        if commit_args:\n            db.session.commit()\n","sourceCodeStart":257,"sourceCodeEnd":293,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/controllers/console/explore/installed_app.py#L257-L293","documentation":"HTTP 400 BadRequest raised by DELETE /console/explore/installed-apps/<installed_app_id> when installed_app.app_owner_tenant_id == current_tenant_id. The Explore uninstall flow is only for apps installed FROM another tenant; the owning tenant cannot 'uninstall' its own app here.","triggerScenarios":"DELETE /console/explore/installed-apps/<id> where the current tenant is the original owner of the app. The guard short-circuits before the db.session.delete call.","commonSituations":"UI shows an 'uninstall' button on an app the current tenant actually owns; confusion between 'my workspace apps' and 'Explore-installed apps'; client reuse of the installed-app tile for owned apps.","solutions":["Do not show the Uninstall action when current_tenant_id === app.app_owner_tenant_id (use the uninstallable flag returned by GET installed-apps).","For owned apps, use the proper app-deletion endpoint under /console/apps, not the Explore uninstall endpoint.","Filter the installed-apps list client-side to only offer uninstall on rows where uninstallable === false (per response semantics) — confirm against the actual field meaning.","If the ownership mapping is wrong, verify app_owner_tenant_id was set correctly at install time."],"exampleFix":"// before: uninstall button shown for every installed app\n<button onClick={() => del(`/installed-apps/${a.id}`)}>Uninstall</button>\n\n// after: only show for apps owned by another tenant\n{!a.uninstallable && a.app_owner_tenant_id !== currentTenantId && (\n  <button onClick={() => del(`/installed-apps/${a.id}`)}>Uninstall</button>\n)}","handlingStrategy":"validation","validationCode":"// Only call uninstall when the current tenant is NOT the owner\nconst isOwnable = installedApp.app_owner_tenant_id === currentTenantId;\nif (isOwnable) {\n  throw new Error('Use the app-deletion flow for apps your tenant owns');\n}","typeGuard":"function isUninstallable(installedApp, currentTenantId) {\n  return installedApp.app_owner_tenant_id !== currentTenantId;\n}","tryCatchPattern":"try {\n  await del(`/console/explore/installed-apps/${id}`);\n} catch (e) {\n  if (e.status === 400 && /owned by the current tenant/.test(e.message)) {\n    hideUninstallButton(id); // not uninstallable via Explore\n  } else throw e;\n}","preventionTips":["Drive Uninstall visibility from the response's uninstallable/ownership fields.","Keep owned-app management on /console/apps, not the Explore flow.","Refresh the ownership flag after a tenant switch."],"tags":["rest-api","explore","bad-request","installed-app","authorization","dify"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}