{"record":{"id":"a90ff2346dc491e1","repo":"affaan-m/ECC","slug":"user-not-found","errorCode":null,"errorMessage":"User not found","messagePattern":"User not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"skills/fastapi-patterns/SKILL.md","lineNumber":283,"sourceCode":"    return UserListResponse(total=total, items=users)\n\n\n@router.patch(\"/{user_id}\", response_model=UserResponse)\nasync def update_user(\n    user_id: int,\n    payload: UserUpdate,\n    db: DbDep,\n    current_user: ActiveUserDep,\n) -> UserResponse:\n    if current_user.id != user_id:\n        raise HTTPException(status_code=403, detail=\"Not authorized\")\n    service = UserService(db)\n    try:\n        user = await service.update(user_id, payload)\n    except DuplicateUserError:\n        raise HTTPException(status_code=400, detail=\"Email already registered\")\n    if user is None:\n        raise HTTPException(status_code=404, detail=\"User not found\")\n    return user\n\n\n@router.post(\"/token\")\nasync def login(\n    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],\n    db: DbDep,\n) -> dict[str, str]:\n    service = UserService(db)\n    token = await service.authenticate(form_data.username, form_data.password)\n    if token is None:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Incorrect username or password\",\n            headers={\"WWW-Authenticate\": \"Bearer\"},\n        )\n    return {\"access_token\": token, \"token_type\": \"bearer\"}\n```","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/fastapi-patterns/SKILL.md#L265-L301","documentation":"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.","triggerScenarios":"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.","commonSituations":"Account deleted (soft or hard) while session was active. Test DB cleaned mid-run. Off-by-one in id construction.","solutions":["Front-end: on 404 with this detail, force logout and redirect to login — the JWT references a non-existent user.","Have `UserService.update` raise a domain `UserNotFoundError` and map it here, for symmetry with `DuplicateUserError`.","If soft-deleted users should be recoverable, return 410 Gone instead and surface a restore flow.","Verify the id source: the JWT `sub` claim should match the row."],"exampleFix":"# before\nif user is None:\n    raise HTTPException(status_code=404, detail=\"User not found\")\n\n# after — let the service raise, map consistently\nfrom app.services.user_service import UserNotFoundError\ntry:\n    user = await service.update(user_id, payload)\nexcept UserNotFoundError:\n    raise HTTPException(status_code=404, detail=\"User not found\")","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"from fastapi import HTTPException\ntry:\n    updated = await client.patch(f'/users/{uid}', json=payload)\nexcept HTTPException as e:\n    if e.status_code == 404:\n        logout_and_redirect_login()\n    raise","preventionTips":["Have the service raise `UserNotFoundError`; map it consistently across routes.","On 404, treat the JWT as stale: force logout.","Verify the JWT `sub` matches a live row before issuing long-lived tokens."],"tags":["fastapi","http-404","services","users","error-mapping"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}