affaan-m/ECC · warning · HTTPException

Email already registered

Error message

Email already registered

What it means

In the `create_user` route handler, the service layer raises a domain exception `DuplicateUserError`; the handler maps it to `HTTPException(400, "Email already registered")`. The 400 (not 409) is the documented contract for this codebase — `DuplicateUserError` is the canonical "email taken" signal from `UserService.create`.

Source

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

# app/routers/users.py
from typing import Annotated
from fastapi import APIRouter, HTTPException, Query, status
from fastapi.security import OAuth2PasswordRequestForm

from app.dependencies import ActiveUserDep, DbDep
from app.schemas.user import UserCreate, UserResponse, UserUpdate, UserListResponse
from app.services.user_service import DuplicateUserError, UserService

router = APIRouter()


@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, db: DbDep) -> UserResponse:
    service = UserService(db)
    try:
        return await service.create(payload)
    except DuplicateUserError:
        raise HTTPException(status_code=400, detail="Email already registered")


@router.get("/me", response_model=UserResponse)
async def get_me(current_user: ActiveUserDep) -> UserResponse:
    return current_user


@router.get("/", response_model=UserListResponse)
async def list_users(
    db: DbDep,
    current_user: ActiveUserDep,
    skip: Annotated[int, Query(ge=0)] = 0,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
) -> UserListResponse:
    service = UserService(db)
    users, total = await service.list(skip=skip, limit=limit)
    return UserListResponse(total=total, items=users)

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Front-end: on 400 with this detail, show "an account with this email already exists" and offer login/password-reset.
  2. Confirm there is a unique constraint on `users.email` at the DB level — without it `DuplicateUserError` can never fire and you get a silent second row.
  3. Make sure `UserService.create` actually raises `DuplicateUserError` on `IntegrityError` (and not a generic 500).
  4. For races, consider 409 Conflict if your API standard prefers it — but keep the service exception the source of truth.

Example fix

# before
except DuplicateUserError:
    raise HTTPException(status_code=400, detail="Email already registered")

# after — 409 is more conventional for conflict; keep detail stable for clients
except DuplicateUserError:
    raise HTTPException(status_code=409, detail="Email already registered")
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

from fastapi import HTTPException
try:
    resp = await client.post('/users/', json=payload)
except HTTPException as e:
    if e.status_code == 400 and 'already registered' in e.detail:
        suggest_login(payload['email'])
    raise

Prevention

When it happens

Trigger: POST `/users/` (or `/`) with an `email` that already exists in the users table — `UserService.create` inserts, hits the unique constraint, and raises `DuplicateUserError`. Race condition: two concurrent registrations for the same email.

Common situations: User registers twice (typo, forgot they had an account). Test suite does not clean up users between runs. Seed data inserts an email a test then tries to create.

Related errors


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