Compare commits

...

5 Commits

Author SHA1 Message Date
Nik
07a5eb3e69 fix: use CREDENTIAL_INVALID for wrong current password 2026-03-12 13:32:40 -07:00
Nik
e04936b83e docs: document detail backward-compat field in OnyxError response shape 2026-03-12 13:19:05 -07:00
Nik
76d430242a fix: use semantic error codes for token refresh and SAML auth failures
- VALIDATION_ERROR → INVALID_TOKEN for token refresh failures
- e.status_code check → e.error_code check for disposable email detection
- HTTPException → OnyxError(UNAUTHENTICATED) for SAML auth failures
2026-03-12 13:06:15 -07:00
Nik
f2b5ff095a fix: add detail field to OnyxError response for frontend compatibility
The frontend auth forms read `response.json().detail` for error messages.
OnyxError responses only had `error_code` and `message` keys, causing
the frontend to show "Unknown error" for all validation failures.

- Add `detail` key (mirrors `message`) to OnyxErrorCode.detail() output
- Update frontend auth forms to also read `.message` as fallback
2026-03-10 20:29:31 -07:00
Nik
77a3243b80 refactor: replace HTTPException with OnyxError in auth, chat, and federated
Migrate auth/users.py, query_and_chat/chat_backend.py,
query_and_chat/token_limit.py, and federated/api.py to use
OnyxError with standardized error codes instead of raw HTTPException.
2026-03-05 17:03:49 -08:00
11 changed files with 188 additions and 176 deletions

View File

@@ -23,11 +23,9 @@ from email_validator import EmailUndeliverableError
from email_validator import validate_email
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Query
from fastapi import Request
from fastapi import Response
from fastapi import status
from fastapi.responses import RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm
from fastapi_users import BaseUserManager
@@ -119,6 +117,8 @@ from onyx.db.models import Persona
from onyx.db.models import User
from onyx.db.pat import fetch_user_for_pat
from onyx.db.users import get_user_by_email
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.redis.redis_pool import get_async_redis_connection
from onyx.server.settings.store import load_settings
from onyx.server.utils import BasicAuthenticationError
@@ -137,8 +137,6 @@ from shared_configs.contextvars import get_current_tenant_id
logger = setup_logger()
REGISTER_INVITE_ONLY_CODE = "REGISTER_INVITE_ONLY"
def is_user_admin(user: User) -> bool:
return user.role == UserRole.ADMIN
@@ -227,17 +225,17 @@ def verify_email_is_invited(email: str) -> None:
whitelist = get_invited_users()
if not email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"reason": "Email must be specified"},
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Email must be specified",
)
try:
email_info = validate_email(email, check_deliverability=False)
except EmailUndeliverableError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"reason": "Email is not valid"},
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Email is not valid",
)
for email_whitelist in whitelist:
@@ -255,12 +253,9 @@ def verify_email_is_invited(email: str) -> None:
if email_info.normalized.lower() == email_info_whitelist.normalized.lower():
return
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"code": REGISTER_INVITE_ONLY_CODE,
"reason": "This workspace is invite-only. Please ask your admin to invite you.",
},
raise OnyxError(
OnyxErrorCode.UNAUTHORIZED,
"This workspace is invite-only. Please ask your admin to invite you.",
)
@@ -272,9 +267,9 @@ def verify_email_in_whitelist(email: str, tenant_id: str) -> None:
def verify_email_domain(email: str) -> None:
if email.count("@") != 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email is not valid",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Email is not valid",
)
local_part, domain = email.split("@")
@@ -283,39 +278,35 @@ def verify_email_domain(email: str) -> None:
if AUTH_TYPE == AuthType.CLOUD:
# Normalize googlemail.com to gmail.com (they deliver to the same inbox)
if domain == "googlemail.com":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"reason": "Please use @gmail.com instead of @googlemail.com."},
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Please use @gmail.com instead of @googlemail.com.",
)
if "+" in local_part and domain != "onyx.app":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"reason": "Email addresses with '+' are not allowed. Please use your base email address."
},
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Email addresses with '+' are not allowed. Please use your base email address.",
)
# Check if email uses a disposable/temporary domain
if is_disposable_email(email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"reason": "Disposable email addresses are not allowed. Please use a permanent email address."
},
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Disposable email addresses are not allowed. Please use a permanent email address.",
)
# Check domain whitelist if configured
if VALID_EMAIL_DOMAINS:
if domain not in VALID_EMAIL_DOMAINS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email domain is not valid",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Email domain is not valid",
)
def enforce_seat_limit(db_session: Session, seats_needed: int = 1) -> None:
"""Raise HTTPException(402) if adding users would exceed the seat limit.
"""Raise OnyxError if adding users would exceed the seat limit.
No-op for multi-tenant or CE deployments.
"""
@@ -327,7 +318,7 @@ def enforce_seat_limit(db_session: Session, seats_needed: int = 1) -> None:
)(db_session, seats_needed=seats_needed)
if result is not None and not result.available:
raise HTTPException(status_code=402, detail=result.error_message)
raise OnyxError(OnyxErrorCode.SEAT_LIMIT_EXCEEDED, result.error_message)
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
@@ -380,9 +371,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
captcha_token or "", expected_action="signup"
)
except CaptchaVerificationError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"reason": str(e)},
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
str(e),
)
# We verify the password here to make sure it's valid before we proceed
@@ -394,11 +385,11 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# This prevents creating tenants for throwaway email addresses
try:
verify_email_domain(user_create.email)
except HTTPException as e:
except OnyxError as e:
# Log blocked disposable email attempts
if (
e.status_code == status.HTTP_400_BAD_REQUEST
and "Disposable email" in str(e.detail)
e.error_code == OnyxErrorCode.VALIDATION_ERROR
and "Disposable email" in str(e.message)
):
domain = (
user_create.email.split("@")[-1]
@@ -623,7 +614,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
)
if not tenant_id:
raise HTTPException(status_code=401, detail="User not found")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "User not found")
# Proceed with the tenant context
token = None
@@ -863,8 +854,8 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
logger.error(
"Email is not configured. Please configure email in the admin panel"
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
raise OnyxError(
OnyxErrorCode.INTERNAL_ERROR,
"Your admin has not enabled this feature.",
)
tenant_id = await fetch_ee_implementation_or_noop(
@@ -964,12 +955,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
old_password, user.hashed_password
)
if not verified:
# Raise some HTTPException (or your custom exception) if old password is invalid:
from fastapi import HTTPException, status
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid current password",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_INVALID,
"Invalid current password",
)
# If the hash was upgraded behind the scenes, we can keep it before setting the new password:
@@ -1239,11 +1227,7 @@ class FastAPIUserWithLogoutRouter(FastAPIUsers[models.UP, models.ID]):
)
logout_responses: OpenAPIResponseType = {
**{
status.HTTP_401_UNAUTHORIZED: {
"description": "Missing token or inactive user."
}
},
**{401: {"description": "Missing token or inactive user."}},
**backend.transport.get_openapi_logout_responses_success(),
}
@@ -1277,11 +1261,7 @@ class FastAPIUserWithLogoutRouter(FastAPIUsers[models.UP, models.ID]):
)
refresh_responses: OpenAPIResponseType = {
**{
status.HTTP_401_UNAUTHORIZED: {
"description": "Missing token or inactive user."
}
},
**{401: {"description": "Missing token or inactive user."}},
**backend.transport.get_openapi_login_responses_success(),
}
@@ -1334,9 +1314,9 @@ class FastAPIUserWithLogoutRouter(FastAPIUsers[models.UP, models.ID]):
return await backend.login(strategy, user)
except Exception as e:
logger.error(f"Unexpected error in refresh endpoint: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Token refresh failed: {str(e)}",
raise OnyxError(
OnyxErrorCode.INVALID_TOKEN,
f"Token refresh failed: {str(e)}",
)
return router
@@ -1759,7 +1739,7 @@ def get_oauth_router(
name=callback_route_name,
description="The response varies based on the authentication backend used.",
responses={
status.HTTP_400_BAD_REQUEST: {
400: {
"model": ErrorModel,
"content": {
"application/json": {
@@ -1792,24 +1772,24 @@ def get_oauth_router(
)
if account_email is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.OAUTH_NOT_AVAILABLE_EMAIL,
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
ErrorCode.OAUTH_NOT_AVAILABLE_EMAIL,
)
try:
state_data = decode_jwt(state, state_secret, [STATE_TOKEN_AUDIENCE])
except jwt.DecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=getattr(
raise OnyxError(
OnyxErrorCode.INVALID_TOKEN,
getattr(
ErrorCode, "ACCESS_TOKEN_DECODE_ERROR", "ACCESS_TOKEN_DECODE_ERROR"
),
)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=getattr(
raise OnyxError(
OnyxErrorCode.TOKEN_EXPIRED,
getattr(
ErrorCode,
"ACCESS_TOKEN_ALREADY_EXPIRED",
"ACCESS_TOKEN_ALREADY_EXPIRED",
@@ -1823,9 +1803,9 @@ def get_oauth_router(
or not state_csrf_token
or not secrets.compare_digest(cookie_csrf_token, state_csrf_token)
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=getattr(ErrorCode, "OAUTH_INVALID_STATE", "OAUTH_INVALID_STATE"),
raise OnyxError(
OnyxErrorCode.CSRF_FAILURE,
getattr(ErrorCode, "OAUTH_INVALID_STATE", "OAUTH_INVALID_STATE"),
)
next_url = state_data.get("next_url", "/")
@@ -1853,15 +1833,15 @@ def get_oauth_router(
is_verified_by_default=is_verified_by_default,
)
except UserAlreadyExists:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.OAUTH_USER_ALREADY_EXISTS,
raise OnyxError(
OnyxErrorCode.CONFLICT,
ErrorCode.OAUTH_USER_ALREADY_EXISTS,
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.LOGIN_BAD_CREDENTIALS,
raise OnyxError(
OnyxErrorCode.UNAUTHENTICATED,
ErrorCode.LOGIN_BAD_CREDENTIALS,
)
# Login user

View File

@@ -90,12 +90,23 @@ class OnyxErrorCode(Enum):
def detail(self, message: str | None = None) -> dict[str, str]:
"""Build a structured error detail dict.
Returns a dict like:
{"error_code": "UNAUTHENTICATED", "message": "Token expired"}
Returns a dict like::
{
"error_code": "UNAUTHENTICATED",
"message": "Token expired",
"detail": "Token expired", # backward-compat alias
}
The ``detail`` key mirrors ``message`` for backward compatibility with
clients that read the ``detail`` field from FastAPI's default
``HTTPException`` response shape.
If no message is supplied, the error code itself is used as the message.
"""
msg = message or self.code
return {
"error_code": self.code,
"message": message or self.code,
"message": msg,
"detail": msg,
}

View File

@@ -3,7 +3,9 @@
Raise ``OnyxError`` instead of ``HTTPException`` in business code. A global
FastAPI exception handler (registered via ``register_onyx_exception_handlers``)
converts it into a JSON response with the standard
``{"error_code": "...", "message": "..."}`` shape.
``{"error_code": "...", "message": "...", "detail": "..."}`` shape. The
``detail`` key mirrors ``message`` for backward compatibility with clients
that expect FastAPI's default ``HTTPException`` response format.
Usage::

View File

@@ -4,7 +4,6 @@ from uuid import UUID
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi import Response
from sqlalchemy.orm import Session
@@ -23,6 +22,8 @@ from onyx.db.federated import update_federated_connector
from onyx.db.federated import update_federated_connector_oauth_token
from onyx.db.federated import validate_federated_connector_credentials
from onyx.db.models import User
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.federated_connectors.factory import get_federated_connector
from onyx.federated_connectors.factory import get_federated_connector_cls
from onyx.federated_connectors.interfaces import FederatedConnector
@@ -58,7 +59,7 @@ def _get_federated_connector_instance(
try:
return get_federated_connector(source, credentials)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@router.post("")
@@ -96,11 +97,11 @@ def create_federated_connector(
except ValueError as e:
logger.warning(f"Validation error creating federated connector: {e}")
db_session.rollback()
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
except Exception as e:
logger.error(f"Error creating federated connector: {e}")
db_session.rollback()
raise HTTPException(status_code=500, detail=str(e))
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
@router.get("/{id}/entities")
@@ -113,10 +114,10 @@ def get_entities(
try:
federated_connector = fetch_federated_connector_by_id(id, db_session)
if not federated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
if federated_connector.credentials is None:
raise HTTPException(
status_code=400, detail="Federated connector has no credentials"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
)
connector_instance = _get_federated_connector_instance(
@@ -138,11 +139,11 @@ def get_entities(
return EntitySpecResponse(entities=entities_dict)
except HTTPException:
except OnyxError:
raise
except Exception as e:
logger.error(f"Error fetching entities for federated connector {id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
@router.get("/{id}/credentials/schema")
@@ -155,10 +156,10 @@ def get_credentials_schema(
try:
federated_connector = fetch_federated_connector_by_id(id, db_session)
if not federated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
if federated_connector.credentials is None:
raise HTTPException(
status_code=400, detail="Federated connector has no credentials"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
)
connector_instance = _get_federated_connector_instance(
@@ -181,13 +182,13 @@ def get_credentials_schema(
return CredentialSchemaResponse(credentials=credentials_dict)
except HTTPException:
except OnyxError:
raise
except Exception as e:
logger.error(
f"Error fetching credentials schema for federated connector {id}: {e}"
)
raise HTTPException(status_code=500, detail=str(e))
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
@router.get("/sources/{source}/configuration/schema")
@@ -215,7 +216,7 @@ def get_configuration_schema_by_source(
except Exception as e:
logger.error(f"Error fetching configuration schema for source {source}: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
@router.get("/sources/{source}/credentials/schema")
@@ -242,11 +243,11 @@ def get_credentials_schema_by_source(
return CredentialSchemaResponse(credentials=credentials_dict)
except HTTPException:
except OnyxError:
raise
except Exception as e:
logger.error(f"Error fetching credentials schema for source {source}: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
@router.post("/sources/{source}/credentials/validate")
@@ -262,15 +263,15 @@ def validate_credentials(
)
if not is_valid:
raise HTTPException(status_code=400, detail="Credentials are invalid")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Credentials are invalid")
return is_valid
except HTTPException:
except OnyxError:
raise
except Exception as e:
logger.error(f"Error validating credentials for source {source}: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
@router.head("/{id}/entities/validate")
@@ -284,7 +285,7 @@ def validate_entities(
try:
federated_connector = fetch_federated_connector_by_id(id, db_session)
if not federated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
if federated_connector.credentials is None:
return Response(status_code=400)
@@ -310,7 +311,7 @@ def validate_entities(
else:
return Response(status_code=400)
except HTTPException:
except OnyxError:
raise
except Exception as e:
logger.error(f"Error validating entities for federated connector {id}: {e}")
@@ -326,14 +327,16 @@ def get_authorize_url(
"""Get URL to send the user for OAuth"""
# Validate that the ID is not None or invalid
if id is None or id <= 0:
raise HTTPException(status_code=400, detail="Invalid federated connector ID")
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Invalid federated connector ID"
)
federated_connector = fetch_federated_connector_by_id(id, db_session)
if not federated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
if federated_connector.credentials is None:
raise HTTPException(
status_code=400, detail="Federated connector has no credentials"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
)
# Update credentials to include the correct redirect URI with the connector ID
@@ -379,19 +382,19 @@ def handle_oauth_callback_generic(
# Verify state parameter and get session info
state = callback_data.get("state")
if not state:
raise HTTPException(status_code=400, detail="Missing state parameter")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Missing state parameter")
try:
oauth_session = verify_oauth_state(state)
except ValueError:
logger.exception("Error verifying OAuth state")
raise HTTPException(
status_code=400, detail="Invalid or expired state parameter"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Invalid or expired state parameter"
)
if not oauth_session:
raise HTTPException(
status_code=400, detail="Invalid or expired state parameter"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Invalid or expired state parameter"
)
# Get federated connector ID from the state
@@ -400,19 +403,19 @@ def handle_oauth_callback_generic(
# Validate federated_connector_id is not None
if federated_connector_id is None:
logger.error("OAuth session has null federated_connector_id")
raise HTTPException(
status_code=400,
detail="Invalid OAuth session: missing federated connector ID",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Invalid OAuth session: missing federated connector ID",
)
federated_connector = fetch_federated_connector_by_id(
federated_connector_id, db_session
)
if not federated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
if federated_connector.credentials is None:
raise HTTPException(
status_code=400, detail="Federated connector has no credentials"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
)
connector_instance = _get_federated_connector_instance(
@@ -519,10 +522,10 @@ def get_federated_connector_detail(
"""Get detailed information about a specific federated connector"""
federated_connector = fetch_federated_connector_by_id(id, db_session)
if not federated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
if federated_connector.credentials is None:
raise HTTPException(
status_code=400, detail="Federated connector has no credentials"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
)
# Get OAuth token information for the current user
@@ -581,14 +584,14 @@ def update_federated_connector_endpoint(
)
if not updated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
# Return updated connector details
return get_federated_connector_detail(id, user, db_session)
except ValueError as e:
logger.warning(f"Validation error updating federated connector {id}: {e}")
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@router.delete("/{id}")
@@ -604,7 +607,7 @@ def delete_federated_connector_endpoint(
)
if not success:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
return True
@@ -619,7 +622,7 @@ def disconnect_oauth_token(
# Check if the federated connector exists
federated_connector = fetch_federated_connector_by_id(id, db_session)
if not federated_connector:
raise HTTPException(status_code=404, detail="Federated connector not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
# Find and delete the user's OAuth token
oauth_token = None
@@ -633,6 +636,4 @@ def disconnect_oauth_token(
db_session.commit()
return True
else:
raise HTTPException(
status_code=404, detail="No OAuth token found for this user"
)
raise OnyxError(OnyxErrorCode.NOT_FOUND, "No OAuth token found for this user")

View File

@@ -6,7 +6,6 @@ from uuid import UUID
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Query
from fastapi import Request
from fastapi import Response
@@ -60,6 +59,8 @@ from onyx.db.persona import get_persona_by_id
from onyx.db.usage import increment_usage
from onyx.db.usage import UsageType
from onyx.db.user_file import get_file_id_by_user_file_id
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.file_store.file_store import get_default_file_store
from onyx.llm.constants import LlmProviderNames
from onyx.llm.factory import get_default_llm
@@ -159,7 +160,9 @@ def get_user_chat_sessions(
datetime.datetime.fromisoformat(before) if before is not None else None
)
except ValueError:
raise HTTPException(status_code=422, detail="Invalid 'before' timestamp format")
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Invalid 'before' timestamp format"
)
try:
# Fetch one extra to determine if there are more results
@@ -216,8 +219,8 @@ def update_chat_session_temperature(
update_thread_req.temperature_override < 0
or update_thread_req.temperature_override > 2
):
raise HTTPException(
status_code=400, detail="Temperature must be between 0 and 2"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR, "Temperature must be between 0 and 2"
)
# Additional check for Anthropic models
@@ -227,9 +230,9 @@ def update_chat_session_temperature(
in chat_session.current_alternate_model.lower()
):
if update_thread_req.temperature_override > 1:
raise HTTPException(
status_code=400,
detail="Temperature for Anthropic models must be between 0 and 1",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Temperature for Anthropic models must be between 0 and 1",
)
chat_session.temperature_override = update_thread_req.temperature_override
@@ -285,23 +288,25 @@ def get_chat_session(
include_deleted=True,
)
except ValueError:
raise HTTPException(status_code=404, detail="Chat session not found")
raise OnyxError(OnyxErrorCode.SESSION_NOT_FOUND, "Chat session not found")
if not include_deleted and existing_chat_session.deleted:
raise HTTPException(status_code=404, detail="Chat session has been deleted")
raise OnyxError(
OnyxErrorCode.SESSION_NOT_FOUND, "Chat session has been deleted"
)
if is_shared:
if existing_chat_session.shared_status != ChatSessionSharedStatus.PUBLIC:
raise HTTPException(
status_code=403, detail="Chat session is not shared"
raise OnyxError(
OnyxErrorCode.UNAUTHORIZED, "Chat session is not shared"
)
elif user_id is not None and existing_chat_session.user_id not in (
user_id,
None,
):
raise HTTPException(status_code=403, detail="Access denied")
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, "Access denied")
raise HTTPException(status_code=404, detail="Chat session not found")
raise OnyxError(OnyxErrorCode.SESSION_NOT_FOUND, "Chat session not found")
# for chat-seeding: if the session is unassigned, assign it now. This is done here
# to avoid another back and forth between FE -> BE before starting the first
@@ -384,10 +389,10 @@ def create_new_chat_session(
)
except ValueError as e:
# Project access denied
raise HTTPException(status_code=403, detail=str(e))
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, str(e))
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=400, detail="Invalid Persona provided.")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Invalid Persona provided.")
return CreateChatSessionID(chat_session_id=new_chat_session.id)
@@ -486,7 +491,7 @@ def delete_all_chat_sessions(
try:
delete_all_chat_sessions_for_user(user=user, db_session=db_session)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@router.delete("/delete-chat-session/{session_id}", tags=PUBLIC_API_TAGS)
@@ -506,7 +511,7 @@ def delete_chat_session_by_id(
user_id, session_id, db_session, hard_delete=actual_hard_delete
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
# NOTE: This endpoint is extremely central to the application, any changes to it should be reviewed and approved by an experienced
@@ -711,7 +716,7 @@ def get_max_document_tokens(
is_for_edit=False,
)
except ValueError:
raise HTTPException(status_code=404, detail="Persona not found")
raise OnyxError(OnyxErrorCode.PERSONA_NOT_FOUND, "Persona not found")
return MaxSelectedDocumentTokens(
max_tokens=_get_available_tokens_for_persona(
@@ -743,10 +748,10 @@ def get_available_context_tokens_for_session(
include_deleted=False,
)
except ValueError:
raise HTTPException(status_code=404, detail="Chat session not found")
raise OnyxError(OnyxErrorCode.SESSION_NOT_FOUND, "Chat session not found")
if not chat_session.persona:
raise HTTPException(status_code=400, detail="Chat session has no persona")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Chat session has no persona")
available = _get_available_tokens_for_persona(
persona=chat_session.persona,
@@ -808,7 +813,7 @@ def fetch_chat_file(
file_store = get_default_file_store()
file_record = file_store.read_file_record(file_id)
if not file_record:
raise HTTPException(status_code=404, detail="File not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "File not found")
media_type = file_record.file_type
file_io = file_store.read_file(file_id, mode="b")

View File

@@ -6,7 +6,6 @@ from functools import lru_cache
from dateutil import tz
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session
@@ -18,6 +17,8 @@ from onyx.db.models import ChatSession
from onyx.db.models import TokenRateLimit
from onyx.db.models import User
from onyx.db.token_limit import fetch_all_global_token_rate_limits
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import fetch_versioned_implementation
@@ -62,9 +63,9 @@ def _user_is_rate_limited_by_global() -> None:
global_usage = _fetch_global_usage(global_cutoff_time, db_session)
if _is_rate_limited(global_rate_limits, global_usage):
raise HTTPException(
status_code=429,
detail="Token budget exceeded for organization. Try again later.",
raise OnyxError(
OnyxErrorCode.RATE_LIMITED,
"Token budget exceeded for organization. Try again later.",
)

View File

@@ -7,10 +7,8 @@ from urllib.parse import urlparse
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi import Response
from fastapi import status
from fastapi_users import exceptions
from fastapi_users.authentication import Strategy
from onelogin.saml2.auth import OneLogin_Saml2_Auth # type: ignore
@@ -29,6 +27,8 @@ from onyx.db.auth import get_user_count
from onyx.db.auth import get_user_db
from onyx.db.engine.async_sql_engine import get_async_session_context_manager
from onyx.db.models import User
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
@@ -233,17 +233,17 @@ async def _process_saml_callback(
"Error when processing SAML Response: %s %s"
% (", ".join(errors), auth.get_last_error_reason())
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied. Failed to parse SAML Response.",
raise OnyxError(
OnyxErrorCode.UNAUTHENTICATED,
"Access denied. Failed to parse SAML Response.",
)
if not auth.is_authenticated():
detail = "Access denied. User was not authenticated"
logger.error(detail)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=detail,
raise OnyxError(
OnyxErrorCode.UNAUTHENTICATED,
detail,
)
user_email: str | None = None
@@ -273,9 +273,9 @@ async def _process_saml_callback(
"Received SAML attributes without email: %s",
list(attributes.keys()),
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=detail,
raise OnyxError(
OnyxErrorCode.UNAUTHENTICATED,
detail,
)
user = await upsert_saml_user(email=user_email)

View File

@@ -10,7 +10,9 @@ export const forgotPassword = async (email: string): Promise<void> => {
if (!response.ok) {
const error = await response.json();
const errorMessage =
error?.detail || "An error occurred during password reset.";
error?.detail ||
error?.message ||
"An error occurred during password reset.";
throw new Error(errorMessage);
}
};
@@ -33,7 +35,9 @@ export const resetPassword = async (
throw new Error(error.detail.reason || "Invalid password");
}
const errorMessage =
error?.detail || "An error occurred during password reset.";
error?.detail ||
error?.message ||
"An error occurred during password reset.";
throw new Error(errorMessage);
}
};

View File

@@ -107,13 +107,18 @@ export default function EmailPasswordForm({
if (!response.ok) {
setIsWorking(false);
const errorDetail: any = (await response.json()).detail;
const errorData: any = await response.json();
const errorDetail: any = errorData.detail;
let errorMsg: string = "Unknown error";
if (typeof errorDetail === "object" && errorDetail.reason) {
errorMsg = errorDetail.reason;
} else if (errorDetail === "REGISTER_USER_ALREADY_EXISTS") {
errorMsg =
"An account already exists with the specified email.";
} else if (typeof errorDetail === "string") {
errorMsg = errorDetail;
} else if (errorData.message) {
errorMsg = errorData.message;
}
if (response.status === 429) {
errorMsg = "Too many requests. Please try again later.";

View File

@@ -46,7 +46,8 @@ export default function Verify({ user }: VerifyProps) {
} else {
let errorDetail = "unknown error";
try {
errorDetail = (await response.json()).detail;
const errorData = await response.json();
errorDetail = errorData.detail || errorData.message || "unknown error";
} catch (e) {
console.error("Failed to parse verification error response:", e);
}

View File

@@ -26,7 +26,9 @@ export function RequestNewVerificationEmail({
if (response.ok) {
toast.success("A new verification email has been sent!");
} else {
const errorDetail = (await response.json()).detail;
const errorData = await response.json();
const errorDetail =
errorData.detail || errorData.message || "Unknown error";
toast.error(
`Failed to send a new verification email - ${errorDetail}`
);