BerriAI/litellm · error · HTTPException
Failed to update user
Error message
Failed to update user
What it means
At the end of _update_single_user_helper, a None response from the Prisma update (no row changed) turns into HTTP 400 {'error': 'Failed to update user'}. This typically means the update's where-clause matched nothing: a proxy_admin updating a non-existent user passes _check_user_update_authz (the 404 silent-create guard only fires for non-admins) and then the write is a no-op, returning None.
Source
Thrown at litellm/proxy/management_endpoints/internal_user_endpoints.py:1492
)
await _invalidate_user_spend_counter_if_changed(non_default_values)
if "object_permission_id" in non_default_values:
await _invalidate_cached_user_entitlement(
user_id=non_default_values.get("user_id"),
object_permission_ids=tuple(
permission_id
for permission_id in (
getattr(existing_user_row, "object_permission_id", None),
non_default_values.get("object_permission_id"),
)
if isinstance(permission_id, str)
),
)
if response is None:
raise HTTPException(
status_code=400,
detail={"error": "Failed to update user"},
)
_strip_password_from_response(response)
return response
def can_user_call_user_update(
user_api_key_dict: UserAPIKeyAuth,
user_info: LiteLLM_UserTable,
) -> bool:
"""
Helper to check if the user has access to the key's info
"""
if (
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
or user_api_key_dict.user_id == user_info.user_id
):View on GitHub (pinned to 77b7c6c40c)
Solutions
- Verify the target exists first (GET /user/list) and correct the user_id/user_email
- Create missing users via POST /user/new instead of relying on /user/update
- Inspect verbose proxy logs around the update to see the Prisma result if the id looks correct
Example fix
# before
POST /user/update -H 'Authorization: Bearer sk-admin' {"user_id": "ghost", "user_alias": "x"}
# 400 Failed to update user
# after
POST /user/new -H 'Authorization: Bearer sk-admin' {"user_id": "ghost"}
POST /user/update -H 'Authorization: Bearer sk-admin' {"user_id": "ghost", "user_alias": "x"} # 200 Defensive patterns
Strategy: try-catch
Validate before calling
import requests
def user_exists(base_url: str, headers: dict, user_id: str) -> bool:
r = requests.get(f"{base_url}/user/list", headers=headers, timeout=10)
r.raise_for_status()
return any(u.get("user_id") == user_id for u in r.json().get("data", [])) Try / catch
except requests.HTTPError as e:
body = e.response.text if e.response is not None else ""
if e.response is not None and e.response.status_code == 400 and "Failed to update user" in body:
# no row matched: verify existence, create via /user/new if intended, then retry once
... Prevention
- Check target existence before admin update calls - the admin path has no not-found guard
- Never rely on /user/update for upsert semantics
- Log which ids failed so partial no-op updates are visible
When it happens
Trigger: POST /user/update as proxy_admin with a user_id (or email resolving to no row) that does not exist in LiteLLM_UserTable; the prisma update call returning None because the filter matched zero rows.
Common situations: Admin upsert-style calls assuming /user/update creates missing users; users deleted mid-flight; identifiers that differ subtly from stored values (encoding, whitespace).
Related errors
- Not connected to DB!
- str(e)
- Failed to update key got response = None
- DB not connected. This endpoint needs a database; set DATABA
- DB not connected. This endpoint needs a database; set DATABA
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/33c236f2a13c61c9.
Report an issue: GitHub.