Compare commits

...

2 Commits

Author SHA1 Message Date
Nik
51e537f2d9 fix(be): use INTERNAL_ERROR for catch-all exceptions in image gen endpoints
Catch-all `except Exception` blocks in create_config and update_config
were using VALIDATION_ERROR (400), which masks server faults as client
errors. Changed to INTERNAL_ERROR (500) per Cubic review feedback.
2026-03-09 15:47:15 -07:00
Nik
8aae07680c refactor(be): migrate search_settings & image_generation to OnyxError
- Replace all HTTPException raises with OnyxError in:
  - backend/onyx/server/manage/search_settings.py (6 occurrences)
  - backend/onyx/server/manage/image_generation/api.py (17 occurrences)
- Update integration tests for new response shape (detail → message)
- "already exists" now returns 409 (DUPLICATE_RESOURCE) instead of 400

Error code mappings:
- 501 NOT_IMPLEMENTED → OnyxErrorCode.NOT_IMPLEMENTED
- 400 validation → OnyxErrorCode.VALIDATION_ERROR
- 404 not found → OnyxErrorCode.NOT_FOUND
- 400 duplicate → OnyxErrorCode.DUPLICATE_RESOURCE (409)
- 400/401 bad creds → OnyxErrorCode.CREDENTIAL_INVALID
2026-03-05 13:18:09 -08:00
4 changed files with 72 additions and 94 deletions

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
@@ -15,6 +14,8 @@ from onyx.db.llm import remove_llm_provider__no_commit
from onyx.db.models import LLMProvider as LLMProviderModel
from onyx.db.models import ModelConfiguration
from onyx.db.models import User
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.image_gen.exceptions import ImageProviderCredentialsError
from onyx.image_gen.factory import get_image_generation_provider
from onyx.image_gen.factory import validate_credentials
@@ -74,9 +75,9 @@ def _build_llm_provider_request(
# Clone mode: Only use API key from source provider
source_provider = db_session.get(LLMProviderModel, source_llm_provider_id)
if not source_provider:
raise HTTPException(
status_code=404,
detail=f"Source LLM provider with id {source_llm_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"Source LLM provider with id {source_llm_provider_id} not found",
)
_validate_llm_provider_change(
@@ -110,9 +111,9 @@ def _build_llm_provider_request(
)
if not provider:
raise HTTPException(
status_code=400,
detail="No provider or source llm provided",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No provider or source llm provided",
)
credentials = ImageGenerationProviderCredentials(
@@ -124,9 +125,9 @@ def _build_llm_provider_request(
)
if not validate_credentials(provider, credentials):
raise HTTPException(
status_code=400,
detail=f"Incorrect credentials for {provider}",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_INVALID,
f"Incorrect credentials for {provider}",
)
return LLMProviderUpsertRequest(
@@ -215,9 +216,9 @@ def test_image_generation(
LLMProviderModel, test_request.source_llm_provider_id
)
if not source_provider:
raise HTTPException(
status_code=404,
detail=f"Source LLM provider with id {test_request.source_llm_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"Source LLM provider with id {test_request.source_llm_provider_id} not found",
)
_validate_llm_provider_change(
@@ -236,9 +237,9 @@ def test_image_generation(
provider = source_provider.provider
if provider is None:
raise HTTPException(
status_code=400,
detail="No provider or source llm provided",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No provider or source llm provided",
)
try:
@@ -257,14 +258,14 @@ def test_image_generation(
),
)
except ValueError:
raise HTTPException(
status_code=404,
detail=f"Invalid image generation provider: {provider}",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"Invalid image generation provider: {provider}",
)
except ImageProviderCredentialsError:
raise HTTPException(
status_code=401,
detail="Invalid image generation credentials",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_INVALID,
"Invalid image generation credentials",
)
quality = _get_test_quality_for_model(test_request.model_name)
@@ -276,15 +277,15 @@ def test_image_generation(
n=1,
quality=quality,
)
except HTTPException:
except OnyxError:
raise
except Exception as e:
# Log only exception type to avoid exposing sensitive data
# (LiteLLM errors may contain URLs with API keys or auth tokens)
logger.warning(f"Image generation test failed: {type(e).__name__}")
raise HTTPException(
status_code=400,
detail=f"Image generation test failed: {type(e).__name__}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Image generation test failed: {type(e).__name__}",
)
@@ -309,9 +310,9 @@ def create_config(
db_session, config_create.image_provider_id
)
if existing_config:
raise HTTPException(
status_code=400,
detail=f"ImageGenerationConfig with image_provider_id '{config_create.image_provider_id}' already exists",
raise OnyxError(
OnyxErrorCode.DUPLICATE_RESOURCE,
f"ImageGenerationConfig with image_provider_id '{config_create.image_provider_id}' already exists",
)
try:
@@ -345,10 +346,10 @@ def create_config(
db_session.commit()
db_session.refresh(config)
return ImageGenerationConfigView.from_model(config)
except HTTPException:
except OnyxError:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception:
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR)
@admin_router.get("/config")
@@ -373,9 +374,9 @@ def get_config_credentials(
"""
config = get_image_generation_config(db_session, image_provider_id)
if not config:
raise HTTPException(
status_code=404,
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
)
return ImageGenerationCredentials.from_model(config)
@@ -401,9 +402,9 @@ def update_config(
# 1. Get existing config
existing_config = get_image_generation_config(db_session, image_provider_id)
if not existing_config:
raise HTTPException(
status_code=404,
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
)
old_llm_provider_id = existing_config.model_configuration.llm_provider_id
@@ -472,10 +473,10 @@ def update_config(
db_session.refresh(existing_config)
return ImageGenerationConfigView.from_model(existing_config)
except HTTPException:
except OnyxError:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception:
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR)
@admin_router.delete("/config/{image_provider_id}")
@@ -489,9 +490,9 @@ def delete_config(
# Get the config first to find the associated LLM provider
existing_config = get_image_generation_config(db_session, image_provider_id)
if not existing_config:
raise HTTPException(
status_code=404,
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
)
llm_provider_id = existing_config.model_configuration.llm_provider_id
@@ -503,10 +504,10 @@ def delete_config(
remove_llm_provider__no_commit(db_session, llm_provider_id)
db_session.commit()
except HTTPException:
except OnyxError:
raise
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
@admin_router.post("/config/{image_provider_id}/default")
@@ -519,7 +520,7 @@ def set_config_as_default(
try:
set_default_image_generation_config(db_session, image_provider_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
@admin_router.delete("/config/{image_provider_id}/default")
@@ -532,4 +533,4 @@ def unset_config_as_default(
try:
unset_default_image_generation_config(db_session, image_provider_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))

View File

@@ -1,7 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import status
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
@@ -27,6 +25,8 @@ from onyx.db.search_settings import update_current_search_settings
from onyx.db.search_settings import update_search_settings_status
from onyx.document_index.factory import get_all_document_indices
from onyx.document_index.factory import get_default_document_index
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.file_processing.unstructured import delete_unstructured_api_key
from onyx.file_processing.unstructured import get_unstructured_api_key
from onyx.file_processing.unstructured import update_unstructured_api_key
@@ -49,36 +49,13 @@ def set_new_search_settings(
_: User = Depends(current_admin_user),
db_session: Session = Depends(get_session), # noqa: ARG001
) -> IdReturn:
"""
Creates a new SearchSettings row and cancels the previous secondary indexing
if any exists.
"""
if search_settings_new.index_name:
logger.warning("Index name was specified by request, this is not suggested")
# Disallow contextual RAG for cloud deployments.
if MULTI_TENANT and search_settings_new.enable_contextual_rag:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Contextual RAG disabled in Onyx Cloud",
)
# Validate cloud provider exists or create new LiteLLM provider.
if search_settings_new.provider_type is not None:
cloud_provider = get_embedding_provider_from_provider_type(
db_session, provider_type=search_settings_new.provider_type
)
if cloud_provider is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"No embedding provider exists for cloud embedding type {search_settings_new.provider_type}",
)
validate_contextual_rag_model(
provider_name=search_settings_new.contextual_rag_llm_provider,
model_name=search_settings_new.contextual_rag_llm_name,
db_session=db_session,
# TODO(andrei): Re-enable.
# NOTE Enable integration external dependency tests in test_search_settings.py
# when this is reenabled. They are currently skipped
logger.error("Setting new search settings is temporarily disabled.")
raise OnyxError(
OnyxErrorCode.NOT_IMPLEMENTED,
"Setting new search settings is temporarily disabled.",
)
search_settings = get_current_search_settings(db_session)
@@ -188,7 +165,7 @@ def delete_search_settings_endpoint(
search_settings_id=deletion_request.search_settings_id,
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@router.get("/get-current-search-settings")
@@ -238,9 +215,9 @@ def update_saved_search_settings(
) -> None:
# Disallow contextual RAG for cloud deployments
if MULTI_TENANT and search_settings.enable_contextual_rag:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Contextual RAG disabled in Onyx Cloud",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Contextual RAG disabled in Onyx Cloud",
)
validate_contextual_rag_model(
@@ -294,7 +271,7 @@ def validate_contextual_rag_model(
model_name=model_name,
db_session=db_session,
):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg)
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, error_msg)
def _validate_contextual_rag_model(

View File

@@ -114,8 +114,8 @@ def test_create_duplicate_config_fails(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "already exists" in response.json()["detail"]
assert response.status_code == 409
assert "already exists" in response.json()["message"]
def test_get_all_configs(
@@ -292,7 +292,7 @@ def test_update_config_source_provider_not_found(
)
assert response.status_code == 404
assert "not found" in response.json()["detail"]
assert "not found" in response.json()["message"]
def test_delete_config(
@@ -468,7 +468,7 @@ def test_create_config_missing_credentials(
)
assert response.status_code == 400
assert "No provider or source llm provided" in response.json()["detail"]
assert "No provider or source llm provided" in response.json()["message"]
def test_create_config_source_provider_not_found(
@@ -488,4 +488,4 @@ def test_create_config_source_provider_not_found(
)
assert response.status_code == 404
assert "not found" in response.json()["detail"]
assert "not found" in response.json()["message"]

View File

@@ -299,7 +299,7 @@ def test_update_contextual_rag_nonexistent_provider(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "Provider nonexistent-provider not found" in response.json()["detail"]
assert "Provider nonexistent-provider not found" in response.json()["message"]
def test_update_contextual_rag_nonexistent_model(
@@ -321,7 +321,7 @@ def test_update_contextual_rag_nonexistent_model(
assert response.status_code == 400
assert (
f"Model nonexistent-model not found in provider {llm_provider.name}"
in response.json()["detail"]
in response.json()["message"]
)
@@ -341,7 +341,7 @@ def test_update_contextual_rag_missing_provider_name(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "Provider name and model name are required" in response.json()["detail"]
assert "Provider name and model name are required" in response.json()["message"]
def test_update_contextual_rag_missing_model_name(
@@ -361,7 +361,7 @@ def test_update_contextual_rag_missing_model_name(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "Provider name and model name are required" in response.json()["detail"]
assert "Provider name and model name are required" in response.json()["message"]
def test_set_new_search_settings_with_contextual_rag(