langgenius/dify · error · BadRequest

You can't uninstall an app owned by the current tenant

Error message

You can't uninstall an app owned by the current tenant

What it means

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.

Source

Thrown at api/controllers/console/explore/installed_app.py:275

        if current_user.current_tenant is None:
            raise ValueError("current_user.current_tenant must not be None")

        current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant, session=db.session())
        return dump_response(
            InstalledAppResponse,
            _installed_app_response_data(
                installed_app,
                app_model,
                current_tenant_id=current_tenant_id,
                current_user=current_user,
            ),
        )

    @console_ns.response(204, "App uninstalled successfully")
    @with_current_tenant_id
    def delete(self, current_tenant_id: str, installed_app: InstalledApp):
        if installed_app.app_owner_tenant_id == current_tenant_id:
            raise BadRequest("You can't uninstall an app owned by the current tenant")

        db.session.delete(installed_app)
        db.session.commit()

        return "", 204

    @console_ns.response(200, "Success", console_ns.models[SimpleResultMessageResponse.__name__])
    @console_ns.expect(console_ns.models[InstalledAppUpdatePayload.__name__])
    @model_validate(InstalledAppUpdatePayload)
    def patch(self, req_data: InstalledAppUpdatePayload, installed_app: InstalledApp):
        commit_args = False
        if req_data.is_pinned is not None:
            installed_app.is_pinned = req_data.is_pinned
            commit_args = True

        if commit_args:
            db.session.commit()

View on GitHub (pinned to ef8544b173)

Solutions

  1. Do not show the Uninstall action when current_tenant_id === app.app_owner_tenant_id (use the uninstallable flag returned by GET installed-apps).
  2. For owned apps, use the proper app-deletion endpoint under /console/apps, not the Explore uninstall endpoint.
  3. 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.
  4. If the ownership mapping is wrong, verify app_owner_tenant_id was set correctly at install time.

Example fix

// before: uninstall button shown for every installed app
<button onClick={() => del(`/installed-apps/${a.id}`)}>Uninstall</button>

// after: only show for apps owned by another tenant
{!a.uninstallable && a.app_owner_tenant_id !== currentTenantId && (
  <button onClick={() => del(`/installed-apps/${a.id}`)}>Uninstall</button>
)}
Defensive patterns

Strategy: validation

Validate before calling

// Only call uninstall when the current tenant is NOT the owner
const isOwnable = installedApp.app_owner_tenant_id === currentTenantId;
if (isOwnable) {
  throw new Error('Use the app-deletion flow for apps your tenant owns');
}

Type guard

function isUninstallable(installedApp, currentTenantId) {
  return installedApp.app_owner_tenant_id !== currentTenantId;
}

Try / catch

try {
  await del(`/console/explore/installed-apps/${id}`);
} catch (e) {
  if (e.status === 400 && /owned by the current tenant/.test(e.message)) {
    hideUninstallButton(id); // not uninstallable via Explore
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12). Data as JSON: /api/errors/518b791442b299dc. Report an issue: GitHub.