Compare commits

...

1 Commits

Author SHA1 Message Date
Nik
5e43bf7c5b fix(be): replace HTTPException with OnyxError in DB layer
Migrate 10 DB layer files from HTTPException to OnyxError for
consistent structured error responses. Also fixes several incorrect
status codes:

- input_prompt: 401 → 403 for authorization errors, 422 → 404 for
  not-found
- connector_credential_pair: 401 → 404 for credential not found
- feedback: 400 → 403 for authorization errors
- chat: 400 → 404 for session not found
- user_group: 400 → 403 for permission errors

Files: onyx/db/{input_prompt,users,connector_credential_pair,feedback,
persona,chat,projects}.py, onyx/db/engine/{sql_engine,async_sql_engine}.py,
ee/onyx/db/user_group.py
2026-03-09 15:47:49 -07:00
10 changed files with 94 additions and 90 deletions

View File

@@ -2,7 +2,6 @@ from collections.abc import Sequence
from operator import and_
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy import delete
from sqlalchemy import func
from sqlalchemy import Select
@@ -37,6 +36,8 @@ from onyx.db.models import UserGroup
from onyx.db.models import UserGroup__ConnectorCredentialPair
from onyx.db.models import UserRole
from onyx.db.users import fetch_user_by_id
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
logger = setup_logger()
@@ -166,18 +167,12 @@ def validate_object_creation_for_user(
if object_is_public and user.role == UserRole.BASIC:
detail = "User does not have permission to create public objects"
logger.error(detail)
raise HTTPException(
status_code=400,
detail=detail,
)
raise OnyxError(OnyxErrorCode.INSUFFICIENT_PERMISSIONS, detail)
if not target_group_ids:
detail = "Curators must specify 1+ groups"
logger.error(detail)
raise HTTPException(
status_code=400,
detail=detail,
)
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, detail)
user_curated_groups = fetch_user_groups_for_user(
db_session=db_session,
@@ -190,10 +185,7 @@ def validate_object_creation_for_user(
if not target_group_ids_set.issubset(user_curated_group_ids):
detail = "Curators cannot control groups they don't curate"
logger.error(detail)
raise HTTPException(
status_code=400,
detail=detail,
)
raise OnyxError(OnyxErrorCode.INSUFFICIENT_PERMISSIONS, detail)
def fetch_user_group(db_session: Session, user_group_id: int) -> UserGroup | None:

View File

@@ -5,7 +5,6 @@ from datetime import timezone
from typing import Tuple
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy import delete
from sqlalchemy import desc
from sqlalchemy import exists
@@ -32,6 +31,8 @@ from onyx.db.models import SearchDoc as DBSearchDoc
from onyx.db.models import ToolCall
from onyx.db.models import User
from onyx.db.persona import get_best_persona_id_for_user
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.file_store.models import FileDescriptor
from onyx.llm.override_models import LLMOverride
@@ -227,7 +228,9 @@ def duplicate_chat_session_for_user_from_slack(
db_session=db_session,
)
if not chat_session:
raise HTTPException(status_code=400, detail="Invalid Chat Session ID provided")
raise OnyxError(
OnyxErrorCode.SESSION_NOT_FOUND, "Invalid Chat Session ID provided"
)
# This enforces permissions and sets a default
new_persona_id = get_best_persona_id_for_user(

View File

@@ -2,7 +2,6 @@ from datetime import datetime
from enum import Enum
from typing import TypeVarTuple
from fastapi import HTTPException
from sqlalchemy import delete
from sqlalchemy import desc
from sqlalchemy import exists
@@ -32,6 +31,8 @@ from onyx.db.models import User
from onyx.db.models import User__UserGroup
from onyx.db.models import UserGroup__ConnectorCredentialPair
from onyx.db.models import UserRole
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.server.models import StatusResponse
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
@@ -539,7 +540,7 @@ def add_credential_to_connector(
)
if connector is None:
raise HTTPException(status_code=404, detail="Connector does not exist")
raise OnyxError(OnyxErrorCode.CONNECTOR_NOT_FOUND, "Connector does not exist")
if access_type == AccessType.SYNC:
if not fetch_ee_implementation_or_noop(
@@ -547,9 +548,9 @@ def add_credential_to_connector(
"check_if_valid_sync_source",
noop_return_value=True,
)(connector.source):
raise HTTPException(
status_code=400,
detail=f"Connector of type {connector.source} does not support SYNC access type",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Connector of type {connector.source} does not support SYNC access type",
)
if credential is None:
@@ -557,9 +558,9 @@ def add_credential_to_connector(
f"Credential {credential_id} does not exist or does not belong to user"
)
logger.error(error_msg)
raise HTTPException(
status_code=401,
detail=error_msg,
raise OnyxError(
OnyxErrorCode.CREDENTIAL_NOT_FOUND,
error_msg,
)
existing_association = (
@@ -622,12 +623,12 @@ def remove_credential_from_connector(
)
if connector is None:
raise HTTPException(status_code=404, detail="Connector does not exist")
raise OnyxError(OnyxErrorCode.CONNECTOR_NOT_FOUND, "Connector does not exist")
if credential is None:
raise HTTPException(
status_code=404,
detail="Credential does not exist or does not belong to user",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_NOT_FOUND,
"Credential does not exist or does not belong to user",
)
association = get_connector_credential_pair_for_user(

View File

@@ -4,7 +4,6 @@ from typing import Any
from typing import AsyncContextManager
import asyncpg # type: ignore
from fastapi import HTTPException
from sqlalchemy import event
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import AsyncEngine
@@ -28,6 +27,8 @@ from onyx.db.engine.sql_engine import build_connection_string
from onyx.db.engine.sql_engine import is_valid_schema_name
from onyx.db.engine.sql_engine import SqlEngine
from onyx.db.engine.sql_engine import USE_IAM_AUTH
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from shared_configs.configs import MULTI_TENANT
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA_STANDARD_VALUE
from shared_configs.contextvars import get_current_tenant_id
@@ -114,7 +115,7 @@ async def get_async_session(
tenant_id = get_current_tenant_id()
if not is_valid_schema_name(tenant_id):
raise HTTPException(status_code=400, detail="Invalid tenant ID")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Invalid tenant ID")
engine = get_sqlalchemy_async_engine()

View File

@@ -6,7 +6,6 @@ from collections.abc import Generator
from contextlib import contextmanager
from typing import Any
from fastapi import HTTPException
from sqlalchemy import event
from sqlalchemy import pool
from sqlalchemy.engine import create_engine
@@ -27,6 +26,8 @@ from onyx.configs.app_configs import POSTGRES_USE_NULL_POOL
from onyx.configs.app_configs import POSTGRES_USER
from onyx.configs.constants import POSTGRES_UNKNOWN_APP_NAME
from onyx.db.engine.iam_auth import provide_iam_token
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.server.utils import BasicAuthenticationError
from onyx.utils.logger import setup_logger
from shared_configs.configs import MULTI_TENANT
@@ -344,7 +345,7 @@ def get_session_with_tenant(*, tenant_id: str) -> Generator[Session, None, None]
engine = get_sqlalchemy_engine()
if not is_valid_schema_name(tenant_id):
raise HTTPException(status_code=400, detail="Invalid tenant ID")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Invalid tenant ID")
# no need to use the schema translation map for self-hosted + default schema
if not MULTI_TENANT and tenant_id == POSTGRES_DEFAULT_SCHEMA_STANDARD_VALUE:
@@ -371,7 +372,7 @@ def get_session() -> Generator[Session, None, None]:
raise BasicAuthenticationError(detail="User must authenticate")
if not is_valid_schema_name(tenant_id):
raise HTTPException(status_code=400, detail="Invalid tenant ID")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Invalid tenant ID")
with get_session_with_current_tenant() as db_session:
yield db_session
@@ -390,7 +391,7 @@ def get_db_readonly_user_session_with_current_tenant() -> (
readonly_engine = get_readonly_sqlalchemy_engine()
if not is_valid_schema_name(tenant_id):
raise HTTPException(status_code=400, detail="Invalid tenant ID")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Invalid tenant ID")
# no need to use the schema translation map for self-hosted + default schema
if not MULTI_TENANT and tenant_id == POSTGRES_DEFAULT_SCHEMA_STANDARD_VALUE:

View File

@@ -2,7 +2,6 @@ from datetime import datetime
from datetime import timezone
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy import and_
from sqlalchemy import asc
from sqlalchemy import delete
@@ -26,6 +25,8 @@ from onyx.db.models import User
from onyx.db.models import User__UserGroup
from onyx.db.models import UserGroup__ConnectorCredentialPair
from onyx.db.models import UserRole
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
logger = setup_logger()
@@ -134,8 +135,9 @@ def update_document_boost_for_user(
stmt = _add_user_filters(stmt, user, get_editable=True)
result: DbDocument | None = db_session.execute(stmt).scalar_one_or_none()
if result is None:
raise HTTPException(
status_code=400, detail="Document is not editable by this user"
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
"Document is not editable by this user",
)
result.boost = boost
@@ -156,8 +158,9 @@ def update_document_hidden_for_user(
stmt = _add_user_filters(stmt, user, get_editable=True)
result = db_session.execute(stmt).scalar_one_or_none()
if result is None:
raise HTTPException(
status_code=400, detail="Document is not editable by this user"
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
"Document is not editable by this user",
)
result.hidden = hidden

View File

@@ -1,6 +1,5 @@
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
@@ -11,6 +10,8 @@ from sqlalchemy.orm import Session
from onyx.db.models import InputPrompt
from onyx.db.models import InputPrompt__User
from onyx.db.models import User
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.server.features.input_prompt.models import InputPromptSnapshot
from onyx.server.manage.models import UserInfo
from onyx.utils.logger import setup_logger
@@ -54,9 +55,9 @@ def insert_input_prompt(
input_prompt = result.scalar_one_or_none()
if input_prompt is None:
raise HTTPException(
status_code=409,
detail=f"A prompt shortcut with the name '{prompt}' already exists",
raise OnyxError(
OnyxErrorCode.DUPLICATE_RESOURCE,
f"A prompt shortcut with the name '{prompt}' already exists",
)
db_session.commit()
@@ -78,7 +79,9 @@ def update_input_prompt(
raise ValueError(f"No input prompt with id {input_prompt_id}")
if not validate_user_prompt_authorization(user, input_prompt):
raise HTTPException(status_code=401, detail="You don't own this prompt")
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS, "You don't own this prompt"
)
input_prompt.prompt = prompt
input_prompt.content = content
@@ -88,9 +91,9 @@ def update_input_prompt(
db_session.commit()
except IntegrityError:
db_session.rollback()
raise HTTPException(
status_code=409,
detail=f"A prompt shortcut with the name '{prompt}' already exists",
raise OnyxError(
OnyxErrorCode.DUPLICATE_RESOURCE,
f"A prompt shortcut with the name '{prompt}' already exists",
)
return input_prompt
@@ -121,7 +124,7 @@ def remove_public_input_prompt(input_prompt_id: int, db_session: Session) -> Non
raise ValueError(f"No input prompt with id {input_prompt_id}")
if not input_prompt.is_public:
raise HTTPException(status_code=400, detail="This prompt is not public")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "This prompt is not public")
db_session.delete(input_prompt)
db_session.commit()
@@ -140,12 +143,15 @@ def remove_input_prompt(
raise ValueError(f"No input prompt with id {input_prompt_id}")
if input_prompt.is_public and not delete_public:
raise HTTPException(
status_code=400, detail="Cannot delete public prompts with this method"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Cannot delete public prompts with this method",
)
if not validate_user_prompt_authorization(user, input_prompt):
raise HTTPException(status_code=401, detail="You do not own this prompt")
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS, "You do not own this prompt"
)
db_session.delete(input_prompt)
db_session.commit()
@@ -167,7 +173,7 @@ def fetch_input_prompt_by_id(
result = db_session.scalar(query)
if result is None:
raise HTTPException(422, "No input prompt found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "No input prompt found")
return result

View File

@@ -3,7 +3,6 @@ from datetime import datetime
from enum import Enum
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy import exists
from sqlalchemy import func
from sqlalchemy import not_
@@ -38,6 +37,8 @@ from onyx.db.models import User__UserGroup
from onyx.db.models import UserFile
from onyx.db.models import UserGroup
from onyx.db.notification import create_notification
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.server.features.persona.models import FullPersonaSnapshot
from onyx.server.features.persona.models import MinimalPersonaSnapshot
from onyx.server.features.persona.models import PersonaSharedNotificationData
@@ -144,9 +145,9 @@ def fetch_persona_by_id_for_user(
stmt = _add_user_filters(stmt=stmt, user=user, get_editable=get_editable)
persona = db_session.scalars(stmt).one_or_none()
if not persona:
raise HTTPException(
status_code=403,
detail=f"Persona with ID {persona_id} does not exist or user is not authorized to access it",
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
f"Persona with ID {persona_id} does not exist or user is not authorized to access it",
)
return persona
@@ -315,7 +316,7 @@ def create_update_persona(
except ValueError as e:
logger.exception("Failed to create persona")
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
return FullPersonaSnapshot.from_model(persona)

View File

@@ -3,7 +3,6 @@ import uuid
from typing import List
from uuid import UUID
from fastapi import HTTPException
from fastapi import UploadFile
from pydantic import BaseModel
from pydantic import ConfigDict
@@ -20,6 +19,8 @@ from onyx.db.models import Project__UserFile
from onyx.db.models import User
from onyx.db.models import UserFile
from onyx.db.models import UserProject
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.server.documents.connector import upload_files
from onyx.server.features.projects.projects_file_utils import categorize_uploaded_files
from onyx.server.features.projects.projects_file_utils import RejectedFile
@@ -110,7 +111,7 @@ def upload_files_to_user_files_with_indexing(
) -> CategorizedFilesResult:
if project_id is not None and user is not None:
if not check_project_ownership(project_id, user.id, db_session):
raise HTTPException(status_code=404, detail="Project not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Project not found")
categorized_files_result = create_user_files(
files,

View File

@@ -2,7 +2,6 @@ from collections.abc import Sequence
from typing import Any
from uuid import UUID
from fastapi import HTTPException
from fastapi_users.password import PasswordHelper
from sqlalchemy import func
from sqlalchemy import select
@@ -24,6 +23,8 @@ from onyx.db.models import Persona__User
from onyx.db.models import SamlAccount
from onyx.db.models import User
from onyx.db.models import User__UserGroup
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
@@ -44,22 +45,22 @@ def validate_user_role_update(
"""
if current_role == UserRole.SLACK_USER:
raise HTTPException(
status_code=400,
detail="To change a Slack User's role, they must first login to Onyx via the web app.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"To change a Slack User's role, they must first login to Onyx via the web app.",
)
if current_role == UserRole.EXT_PERM_USER:
# This shouldn't happen, but just in case
raise HTTPException(
status_code=400,
detail="To change an External Permissioned User's role, they must first login to Onyx via the web app.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"To change an External Permissioned User's role, they must first login to Onyx via the web app.",
)
if current_role == UserRole.LIMITED:
raise HTTPException(
status_code=400,
detail="To change a Limited User's role, they must first login to Onyx via the web app.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"To change a Limited User's role, they must first login to Onyx via the web app.",
)
if explicit_override:
@@ -67,40 +68,34 @@ def validate_user_role_update(
if requested_role == UserRole.CURATOR:
# This shouldn't happen, but just in case
raise HTTPException(
status_code=400,
detail="Curator role must be set via the User Group Menu",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Curator role must be set via the User Group Menu",
)
if requested_role == UserRole.LIMITED:
# This shouldn't happen, but just in case
raise HTTPException(
status_code=400,
detail=(
"A user cannot be set to a Limited User role. "
"This role is automatically assigned to users through certain endpoints in the API."
),
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"A user cannot be set to a Limited User role. "
"This role is automatically assigned to users through certain endpoints in the API.",
)
if requested_role == UserRole.SLACK_USER:
# This shouldn't happen, but just in case
raise HTTPException(
status_code=400,
detail=(
"A user cannot be set to a Slack User role. "
"This role is automatically assigned to users who only use Onyx via Slack."
),
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"A user cannot be set to a Slack User role. "
"This role is automatically assigned to users who only use Onyx via Slack.",
)
if requested_role == UserRole.EXT_PERM_USER:
# This shouldn't happen, but just in case
raise HTTPException(
status_code=400,
detail=(
"A user cannot be set to an External Permissioned User role. "
"This role is automatically assigned to users who have been "
"pulled in to the system via an external permissions system."
),
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"A user cannot be set to an External Permissioned User role. "
"This role is automatically assigned to users who have been "
"pulled in to the system via an external permissions system.",
)