BerriAI/litellm · error · HTTPException

f"User {user_id} not found"

Error message

f"User {user_id} not found"

What it means

After the ownership check passes, GET /user/info fetches the row with prisma_client.get_data(user_id=...). If no row matches the exact id string, LiteLLM returns 404 'User {user_id} not found'. The lookup is exact-match, so encoding differences ('+' decoded to a space), typos, or a user_email passed where a user_id is expected all miss.

Source

Thrown at litellm/proxy/management_endpoints/internal_user_endpoints.py:888

        user_id = _normalize_user_info_user_id(request=request, user_id=user_id)
        _enforce_user_info_access(user_id=user_id, user_api_key_dict=user_api_key_dict)

        if prisma_client is None:
            raise Exception(
                "Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys"
            )
        if user_id is None and _user_has_admin_view(user_api_key_dict):
            return await _get_user_info_for_proxy_admin(user_api_key_dict=user_api_key_dict)
        elif user_id is None:
            user_id = user_api_key_dict.user_id
        ## GET USER ROW ##

        user_info = None
        if user_id is not None:
            user_info = await prisma_client.get_data(user_id=user_id)

        if user_info is None:
            raise HTTPException(
                status_code=404,
                detail=f"User {user_id} not found",
            )

        team_list, teams_1 = await _get_user_info_teams(
            prisma_client=prisma_client,
            user_id=user_id,
            user_info=user_info,
            user_api_key_dict=user_api_key_dict,
        )

        ## GET ALL KEYS ##
        keys: Final = await prisma_client.get_data(
            user_id=user_id,
            table_name="key",
            query_type="find_all",
        )

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. List users with GET /user/list and copy the exact user_id
  2. URL-encode the id (%2B for '+') so it matches the stored value
  3. Create the user first via POST /user/new (or SSO login) if it must exist

Example fix

# before
GET /user/info?user_id=myuser    # 404 User myuser not found

# after
GET /user/list                   # find exact id, e.g. 'my-user'
GET /user/info?user_id=my-user   # 200
Defensive patterns

Strategy: validation

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:
    if e.response is not None and e.response.status_code == 404 and "not found" in e.response.text.lower():
        return None  # missing resource: skip or re-create, do not blind-retry
    raise

Prevention

When it happens

Trigger: GET /user/info?user_id=<deleted or never-created id>; passing an email in a format that differs from the stored user_id; '+' in the id decoded to a space so the exact-match fetch returns None.

Common situations: Referencing users removed by cleanup jobs; ids copy-pasted with whitespace or encoding damage; querying a user whose /user/new call failed earlier in the flow.

Understand the failure class

Background: "User not found", "Invalid user", and "does not exist": what missing-user lookup errors mean across Rocket.Chat, LiteLLM, Phabricator, rustfs, and pnpm — this error's family across 10 libraries.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/42e5b27ba227ad4d. Report an issue: GitHub.