Skip to content

Exception Context Reference

litestar-auth keeps client-facing HTTP error payloads small and stable, but several domain exceptions now also carry structured context on the exception instance for logging, tracing, and operator diagnostics.

Use the exception attributes for programmatic handling. Do not parse str(exc) to recover structured data.

Serialization boundary

Bundled handlers typically send only the exception message and machine-readable code to the client:

{
  "status_code": 400,
  "detail": "Human-readable message",
  "extra": {
    "code": "USER_ALREADY_EXISTS"
  }
}

The extra context fields documented below stay on the Python exception object unless your application chooses to expose them.

OAuthAccountAlreadyLinkedError

Raised when an OAuth provider identity is already linked to a different local user.

Field Type Semantics
provider str Normalized provider name for the conflicting OAuth account, such as "google" or "github".
account_id str Provider-side account identifier that is already linked.
existing_user_id object Local user identifier that already owns that provider identity.

Notes: - The constructor stores these context values as provided. If your application needs stricter invariants, validate them at the raise site that owns the data. - The default message includes the three context values for operator-facing diagnostics.

Example catch/log flow:

from logging import getLogger

from litestar_auth.db import OAuthAccountData
from litestar_auth.exceptions import OAuthAccountAlreadyLinkedError

logger = getLogger(__name__)

try:
    await user_db.upsert_oauth_account(
        user,
        account=OAuthAccountData(
            oauth_name="google",
            account_id="acct-123",
            account_email="user@example.com",
            access_token="provider-access-token",
            expires_at=None,
            refresh_token=None,
        ),
    )
except OAuthAccountAlreadyLinkedError as exc:
    logger.warning(
        "OAuth account conflict provider=%s account_id=%s existing_user_id=%s",
        exc.provider,
        exc.account_id,
        exc.existing_user_id,
    )
    raise

Typical bundled-controller response shape:

{
  "status_code": 400,
  "detail": "This provider account is already linked to another user. One provider identity can only be linked to a single local account.",
  "extra": {
    "code": "OAUTH_ACCOUNT_ALREADY_LINKED"
  }
}

Directly exposing str(exc) for this exception can reveal the provider account id and the local user id. Keep the structured attributes for trusted operator logs, and map client responses to the generic message shown above.

UserAlreadyExistsError

Raised when a create or update flow collides with an existing user identity.

Field Type Semantics
identifier UserIdentifier | None Structured duplicate-identifier payload when supplied.
identifier_type "email" | "username" | None Convenience mirror of identifier.identifier_type; message-only construction leaves this as None.
identifier_value str | None Convenience mirror of identifier.identifier_value; message-only construction leaves this as None.

Notes: - Construct with UserAlreadyExistsError(identifier=UserIdentifier(...)) or omit identifier. - The default message is generic even when identifier is present. Log identifier_type / identifier_value from trusted operator code when you need collision diagnostics; do not echo them to clients. - message, code, and identifier are keyword-only. - Structured construction stores the supplied identifier context as-is. - The default message does not include the identifier context.

Example catch/log flow:

from logging import getLogger

from litestar_auth.exceptions import UserAlreadyExistsError, UserIdentifier

logger = getLogger(__name__)

try:
    raise UserAlreadyExistsError(
        identifier=UserIdentifier(
            identifier_type="email",
            identifier_value="admin@example.com",
        ),
    )
except UserAlreadyExistsError as exc:
    logger.info(
        "Duplicate user identifier_type=%s identifier_value=%r",
        exc.identifier_type,
        exc.identifier_value,
    )
    raise

Direct domain-error response shape when no endpoint-specific mapper overrides the code:

{
  "status_code": 400,
  "detail": "A user with the provided credentials already exists.",
  "extra": {
    "code": "USER_ALREADY_EXISTS"
  }
}

If your code raises the structured form directly, the default detail remains generic:

{
  "status_code": 400,
  "detail": "A user with the provided credentials already exists.",
  "extra": {
    "code": "USER_ALREADY_EXISTS"
  }
}

InvalidPasswordError

Raised when password validation or verification fails.

Field Type Semantics
user_id object | None Optional operator-only identifier for the user whose password check failed.

Notes: - message, code, and user_id are keyword-only. - user_id is intentionally not included in the default message. - Use this field for internal logging or security monitoring; do not echo it to untrusted clients unless you have explicitly decided to expose it.

Example catch/log flow:

from logging import getLogger

from litestar_auth.exceptions import InvalidPasswordError

logger = getLogger(__name__)

try:
    await user_manager.update(update, user)
except InvalidPasswordError as exc:
    logger.warning("Password validation failed user_id=%s", exc.user_id)
    raise

Typical bundled-controller response shape:

{
  "status_code": 400,
  "detail": "The provided password is invalid.",
  "extra": {
    "code": "UPDATE_USER_INVALID_PASSWORD"
  }
}

InsufficientRolesError

Raised when an authenticated user fails a structured role requirement.

Field Type Semantics
required_roles frozenset[str] Normalized roles required by the authorization check.
user_roles frozenset[str] Normalized roles available on the authenticated user.
require_all bool True when every role in required_roles must be present; False when any one role is sufficient.

Notes: - The constructor stores the supplied role sets as-is; validate role-name invariants at the raise site if your application requires them. - The default message is intentionally generic so ordinary logs and HTTP responses do not leak internal role names. - The built-in role guards raise this exception directly, and the bundled plugin exception wiring maps it to HTTP 403 with code only by default. Custom exception hooks can still inspect required_roles, user_roles, and require_all on the exception instance when operator-facing diagnostics need that detail.

Example catch/log flow:

from logging import getLogger

from litestar_auth.exceptions import ErrorCode, InsufficientRolesError

logger = getLogger(__name__)

try:
    raise InsufficientRolesError(
        required_roles=frozenset({"admin", "billing"}),
        user_roles=frozenset({"support"}),
        require_all=True,
    )
except InsufficientRolesError as exc:
    logger.info(
        "Role denial require_all=%s required_roles=%s user_roles=%s",
        exc.require_all,
        sorted(exc.required_roles),
        sorted(exc.user_roles),
    )
    raise

Example custom-handler response shape:

{
  "status_code": 403,
  "detail": "The authenticated user does not have all of the required roles.",
  "extra": {
    "code": "INSUFFICIENT_ROLES"
  }
}