{"record":{"id":"33c236f2a13c61c9","repo":"BerriAI/litellm","slug":"failed-to-update-user","errorCode":null,"errorMessage":"Failed to update user","messagePattern":"Failed to update user","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/management_endpoints/internal_user_endpoints.py","lineNumber":1492,"sourceCode":"        )\n\n        await _invalidate_user_spend_counter_if_changed(non_default_values)\n\n        if \"object_permission_id\" in non_default_values:\n            await _invalidate_cached_user_entitlement(\n                user_id=non_default_values.get(\"user_id\"),\n                object_permission_ids=tuple(\n                    permission_id\n                    for permission_id in (\n                        getattr(existing_user_row, \"object_permission_id\", None),\n                        non_default_values.get(\"object_permission_id\"),\n                    )\n                    if isinstance(permission_id, str)\n                ),\n            )\n\n    if response is None:\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": \"Failed to update user\"},\n        )\n    _strip_password_from_response(response)\n    return response\n\n\ndef can_user_call_user_update(\n    user_api_key_dict: UserAPIKeyAuth,\n    user_info: LiteLLM_UserTable,\n) -> bool:\n    \"\"\"\n    Helper to check if the user has access to the key's info\n    \"\"\"\n    if (\n        user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value\n        or user_api_key_dict.user_id == user_info.user_id\n    ):","sourceCodeStart":1474,"sourceCodeEnd":1510,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/internal_user_endpoints.py#L1474-L1510","documentation":"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.","triggerScenarios":"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.","commonSituations":"Admin upsert-style calls assuming /user/update creates missing users; users deleted mid-flight; identifiers that differ subtly from stored values (encoding, whitespace).","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"],"exampleFix":"# before\nPOST /user/update -H 'Authorization: Bearer sk-admin' {\"user_id\": \"ghost\", \"user_alias\": \"x\"}\n# 400 Failed to update user\n\n# after\nPOST /user/new   -H 'Authorization: Bearer sk-admin' {\"user_id\": \"ghost\"}\nPOST /user/update -H 'Authorization: Bearer sk-admin' {\"user_id\": \"ghost\", \"user_alias\": \"x\"}  # 200","handlingStrategy":"try-catch","validationCode":"import requests\n\ndef user_exists(base_url: str, headers: dict, user_id: str) -> bool:\n    r = requests.get(f\"{base_url}/user/list\", headers=headers, timeout=10)\n    r.raise_for_status()\n    return any(u.get(\"user_id\") == user_id for u in r.json().get(\"data\", []))","typeGuard":null,"tryCatchPattern":"except requests.HTTPError as e:\n    body = e.response.text if e.response is not None else \"\"\n    if e.response is not None and e.response.status_code == 400 and \"Failed to update user\" in body:\n        # no row matched: verify existence, create via /user/new if intended, then retry once\n        ...","preventionTips":["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"],"tags":["litellm","user-management","update","database"],"backgroundTag":"database-update-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}