affaan-m/ECC · error · HTTPException

User not found

Error message

User not found

What it means

In `update_user`, after `UserService.update` returns without raising, the handler checks `if user is None` and raises `HTTPException(404, "User not found")`. The service returns `None` (rather than raising) to signal the target row did not exist. Note the ownership check at 586 runs first, so a non-owner always gets 403 even if the id does not exist.

Source

Thrown at skills/fastapi-patterns/SKILL.md:283

    return UserListResponse(total=total, items=users)


@router.patch("/{user_id}", response_model=UserResponse)
async def update_user(
    user_id: int,
    payload: UserUpdate,
    db: DbDep,
    current_user: ActiveUserDep,
) -> UserResponse:
    if current_user.id != user_id:
        raise HTTPException(status_code=403, detail="Not authorized")
    service = UserService(db)
    try:
        user = await service.update(user_id, payload)
    except DuplicateUserError:
        raise HTTPException(status_code=400, detail="Email already registered")
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return user


@router.post("/token")
async def login(
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
    db: DbDep,
) -> dict[str, str]:
    service = UserService(db)
    token = await service.authenticate(form_data.username, form_data.password)
    if token is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return {"access_token": token, "token_type": "bearer"}
```

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Front-end: on 404 with this detail, force logout and redirect to login — the JWT references a non-existent user.
  2. Have `UserService.update` raise a domain `UserNotFoundError` and map it here, for symmetry with `DuplicateUserError`.
  3. If soft-deleted users should be recoverable, return 410 Gone instead and surface a restore flow.
  4. Verify the id source: the JWT `sub` claim should match the row.

Example fix

# before
if user is None:
    raise HTTPException(status_code=404, detail="User not found")

# after — let the service raise, map consistently
from app.services.user_service import UserNotFoundError
try:
    user = await service.update(user_id, payload)
except UserNotFoundError:
    raise HTTPException(status_code=404, detail="User not found")
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

from fastapi import HTTPException
try:
    updated = await client.patch(f'/users/{uid}', json=payload)
except HTTPException as e:
    if e.status_code == 404:
        logout_and_redirect_login()
    raise

Prevention

When it happens

Trigger: Authenticated user patches their own id, but the row was deleted between token issuance and the PATCH — `UserService.update` finds nothing and returns `None`. Also if the client sends an id the user owns (per JWT) but the DB was truncated.

Common situations: Account deleted (soft or hard) while session was active. Test DB cleaned mid-run. Off-by-one in id construction.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/a90ff2346dc491e1. Report an issue: GitHub.