fix(users): return precise conflicts without retrying them

This commit is contained in:
Mrinal Chanshetty 2026-11-02 17:20:16 -08:00
parent 5da3813d63
commit dfe290a956
3 changed files with 60 additions and 2 deletions

View file

@ -5559,6 +5559,16 @@ _LOCK_TEAMS_BY_ID_SQL: Final = (
)
def _user_unique_conflict_message(metadata: object, user_id: object) -> str:
match metadata:
case {"target": ["user_id"]}:
return f"User with id {user_id} already exists"
case {"target": ["sso_user_id"]}:
return "User with this sso_user_id already exists"
case _:
return "User conflicts with an existing unique value"
class PrismaClient:
spend_log_transactions: list = []
_spend_log_transactions_lock = asyncio.Lock()
@ -6446,6 +6456,7 @@ class PrismaClient:
max_tries=3, # maximum number of retries
max_time=10, # maximum total time to retry for
on_backoff=on_backoff, # specifying the function to call on backoff
giveup=lambda exc: isinstance(exc, HTTPException) and exc.status_code == 409,
)
@log_db_metrics
async def insert_data(
@ -6504,7 +6515,7 @@ class PrismaClient:
except UniqueViolationError as e:
raise HTTPException(
status_code=409,
detail={"error": f"User with id {db_data['user_id']} already exists"},
detail={"error": _user_unique_conflict_message(e.meta, db_data["user_id"])},
) from e
except Exception as e:
if (

View file

@ -73,7 +73,10 @@ async def test_new_user_returns_conflict_when_id_is_taken_after_duplicate_check(
table: Final = SimpleNamespace(
find_first=AsyncMock(return_value=None),
count=AsyncMock(return_value=0),
create=AsyncMock(side_effect=UniqueViolationError({}, message="Unique constraint failed on user_id")),
create=AsyncMock(side_effect=UniqueViolationError(
{"user_facing_error": {"meta": {"target": ["user_id"]}}},
message="Unique constraint failed on user_id",
)),
upsert=AsyncMock(return_value=LiteLLM_UserTable(user_id="employee-42")),
)
mocker.patch("prisma.Prisma", return_value=SimpleNamespace(litellm_usertable=table))

View file

@ -18,8 +18,10 @@ from types import SimpleNamespace
from typing import Final
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from fastapi import HTTPException
from prisma.errors import UniqueViolationError
from litellm._service_logger import ServiceTypes
from litellm.models.user import LiteLLM_UserTable
@ -192,6 +194,48 @@ async def test_insert_data_team_serializes_members(prisma_client: PrismaClient)
assert create_payload["team_id"] == "t1"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("metadata", "message"),
[
({"target": ["user_id"]}, "User with id requested-user already exists"),
({"target": ["sso_user_id"]}, "User with this sso_user_id already exists"),
(None, "User conflicts with an existing unique value"),
({"target": ["unknown_field"]}, "User conflicts with an existing unique value"),
],
)
async def test_user_conflict_names_constraint_and_does_not_retry(
prisma_client: PrismaClient,
metadata: Mapping[str, object] | None,
message: str,
) -> None:
create: Final = AsyncMock(side_effect=UniqueViolationError({"user_facing_error": {"meta": metadata}}))
prisma_client.db.litellm_usertable.create = create
with pytest.raises(HTTPException) as error:
await prisma_client.insert_data(
data={"user_id": "requested-user", "sso_user_id": "sso-subject"},
table_name="user", allow_existing_user=False,
)
assert error.value.status_code == 409
assert error.value.detail == {"error": message}
assert create.await_count == 1
@pytest.mark.asyncio
async def test_user_insert_still_retries_transient_connection_failure(
prisma_client: PrismaClient, monkeypatch: pytest.MonkeyPatch
) -> None:
row: Final = LiteLLM_UserTable(user_id="requested-user")
create: Final = AsyncMock(side_effect=[httpx.ConnectError("connection unavailable"), row])
prisma_client.db.litellm_usertable.create = create
monkeypatch.setattr("asyncio.sleep", AsyncMock())
result: Final = await prisma_client.insert_data(
data={"user_id": row.user_id}, table_name="user", allow_existing_user=False
)
assert result is row
assert create.await_count == 2
@pytest.mark.asyncio
async def test_insert_data_user_organization_fk_raises_400(
prisma_client: PrismaClient,