Compare commits

...

4 Commits

Author SHA1 Message Date
Nik
57b8e55ae8 fix: use INSUFFICIENT_PERMISSIONS for non-ingestion-API documents 2026-03-12 13:19:33 -07:00
Nik
ec200a3399 fix: use NOT_FOUND for missing connector-credential pair 2026-03-12 13:07:21 -07:00
Nik
e1affc6248 fix: use CONNECTOR_VALIDATION_FAILED for SearXNG API-disabled 429
A SearXNG 429 means the instance is configured to reject API requests,
not that the user is being rate-limited. CONNECTOR_VALIDATION_FAILED
(400) is semantically correct for a misconfigured connector.
2026-03-11 19:05:19 -07:00
Nik
f278047639 refactor: replace HTTPException with OnyxError in remaining misc files
Convert ingestion API, SAML auth, docprocessing tasks, usage limits,
web search clients, EE auth, and enterprise settings to use OnyxError
instead of HTTPException for consistent error handling.
2026-03-05 18:10:45 -08:00
11 changed files with 110 additions and 111 deletions

View File

@@ -2,9 +2,7 @@ from datetime import datetime
import jwt
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi import status
from ee.onyx.configs.app_configs import SUPER_CLOUD_API_KEY
from ee.onyx.configs.app_configs import SUPER_USERS
@@ -13,6 +11,8 @@ from onyx.auth.users import current_admin_user
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.app_configs import USER_AUTH_SECRET
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
@@ -37,12 +37,12 @@ async def current_cloud_superuser(
) -> User:
api_key = request.headers.get("Authorization", "").replace("Bearer ", "")
if api_key != SUPER_CLOUD_API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "Invalid API key")
if user and user.email not in SUPER_USERS:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied. User must be a cloud superuser to perform this action.",
raise OnyxError(
OnyxErrorCode.UNAUTHORIZED,
"Access denied. User must be a cloud superuser to perform this action.",
)
return user

View File

@@ -4,7 +4,6 @@ from typing import Any
from typing import cast
from typing import IO
from fastapi import HTTPException
from fastapi import UploadFile
from ee.onyx.server.enterprise_settings.models import AnalyticsScriptUpload
@@ -13,6 +12,8 @@ from onyx.configs.constants import FileOrigin
from onyx.configs.constants import KV_CUSTOM_ANALYTICS_SCRIPT_KEY
from onyx.configs.constants import KV_ENTERPRISE_SETTINGS_KEY
from onyx.configs.constants import ONYX_DEFAULT_APPLICATION_NAME
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.key_value_store.factory import get_kv_store
from onyx.key_value_store.interface import KvKeyNotFoundError
@@ -118,9 +119,9 @@ def upload_logo(file: UploadFile | str, is_logotype: bool = False) -> bool:
else:
logger.notice("Uploading logo from uploaded file")
if not file.filename or not is_valid_file_type(file.filename):
raise HTTPException(
status_code=400,
detail="Invalid file type- only .png, .jpg, and .jpeg files are allowed",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Invalid file type- only .png, .jpg, and .jpeg files are allowed",
)
content = file.file
display_name = file.filename

View File

@@ -12,7 +12,6 @@ from celery import Celery
from celery import shared_task
from celery import Task
from celery.exceptions import SoftTimeLimitExceeded
from fastapi import HTTPException
from pydantic import BaseModel
from redis import Redis
from redis.lock import Lock as RedisLock
@@ -88,6 +87,7 @@ from onyx.db.search_settings import get_current_search_settings
from onyx.db.search_settings import get_secondary_search_settings
from onyx.db.swap_index import check_and_perform_index_swap
from onyx.document_index.factory import get_all_document_indices
from onyx.error_handling.exceptions import OnyxError
from onyx.file_store.document_batch_storage import DocumentBatchStorage
from onyx.file_store.document_batch_storage import get_document_batch_storage
from onyx.httpx.httpx_pool import HttpxPool
@@ -1323,7 +1323,7 @@ def _docprocessing_task(
if USAGE_LIMITS_ENABLED:
try:
_check_chunk_usage_limit(tenant_id)
except HTTPException as e:
except OnyxError as e:
# Log the error and fail the indexing attempt
task_logger.error(
f"Chunk indexing usage limit exceeded for tenant {tenant_id}: {e}"

View File

@@ -3,7 +3,6 @@ from datetime import timezone
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_curator_or_admin_user
@@ -23,6 +22,8 @@ from onyx.db.search_settings import get_active_search_settings
from onyx.db.search_settings import get_current_search_settings
from onyx.db.search_settings import get_secondary_search_settings
from onyx.document_index.factory import get_all_document_indices
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.indexing.adapters.document_indexing_adapter import (
DocumentIndexingBatchAdapter,
)
@@ -98,8 +99,9 @@ def upsert_ingestion_doc(
cc_pair_id=doc_info.cc_pair_id or DEFAULT_CC_PAIR_ID,
)
if cc_pair is None:
raise HTTPException(
status_code=400, detail="Connector-Credential Pair specified does not exist"
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
"Connector-Credential Pair specified does not exist",
)
# Need to index for both the primary and secondary index if possible
@@ -187,12 +189,12 @@ def delete_ingestion_doc(
# Verify the document exists and was created via the ingestion API
document = get_document(document_id=document_id, db_session=db_session)
if document is None:
raise HTTPException(status_code=404, detail="Document not found")
raise OnyxError(OnyxErrorCode.DOCUMENT_NOT_FOUND, "Document not found")
if not document.from_ingestion_api:
raise HTTPException(
status_code=400,
detail="Document was not created via the ingestion API",
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
"Document was not created via the ingestion API",
)
active_search_settings = get_active_search_settings(db_session)

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,18 +233,15 @@ 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.UNAUTHORIZED,
"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.UNAUTHORIZED, detail)
user_email: str | None = None
@@ -273,10 +270,7 @@ 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.UNAUTHORIZED, detail)
user = await upsert_saml_user(email=user_email)

View File

@@ -2,7 +2,6 @@
from collections.abc import Callable
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.configs.app_configs import ANTHROPIC_DEFAULT_API_KEY
@@ -12,6 +11,8 @@ from onyx.configs.app_configs import OPENROUTER_DEFAULT_API_KEY
from onyx.db.usage import check_usage_limit
from onyx.db.usage import UsageLimitExceededError
from onyx.db.usage import UsageType
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.server.tenant_usage_limits import TenantUsageLimitKeys
from onyx.server.tenant_usage_limits import TenantUsageLimitOverrides
from onyx.utils.logger import setup_logger
@@ -185,7 +186,7 @@ def check_llm_cost_limit_for_provider(
llm_provider_api_key: The API key of the LLM provider that will be used
Raises:
HTTPException: 429 Too Many Requests if limit exceeded
OnyxError: RATE_LIMITED if limit exceeded
"""
if not is_usage_limits_enabled():
return
@@ -209,7 +210,7 @@ def check_usage_and_raise(
pending_amount: float | int = 0,
) -> None:
"""
Check if usage limit would be exceeded and raise HTTPException if so.
Check if usage limit would be exceeded and raise OnyxError if so.
Args:
db_session: Database session for the tenant
@@ -218,7 +219,7 @@ def check_usage_and_raise(
pending_amount: Amount about to be used
Raises:
HTTPException: 429 Too Many Requests if limit exceeded
OnyxError: RATE_LIMITED if limit exceeded
"""
if not is_usage_limits_enabled():
return
@@ -267,4 +268,4 @@ def check_usage_and_raise(
"Please upgrade your plan or wait for the next billing period."
)
raise HTTPException(status_code=429, detail=detail)
raise OnyxError(OnyxErrorCode.RATE_LIMITED, detail)

View File

@@ -3,8 +3,9 @@ from __future__ import annotations
from typing import Any
import requests
from fastapi import HTTPException
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.tools.tool_implementations.web_search.models import (
WebSearchProvider,
)
@@ -146,11 +147,11 @@ class BraveClient(WebSearchProvider):
try:
test_results = self.search("test")
if not test_results or not any(result.link for result in test_results):
raise HTTPException(
status_code=400,
detail="Brave API key validation failed: search returned no results.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Brave API key validation failed: search returned no results.",
)
except HTTPException:
except OnyxError:
raise
except (ValueError, requests.RequestException) as e:
error_msg = str(e)
@@ -161,18 +162,18 @@ class BraveClient(WebSearchProvider):
or "api key" in lower
or "auth" in lower
):
raise HTTPException(
status_code=400,
detail=f"Invalid Brave API key: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Invalid Brave API key: {error_msg}",
) from e
if "status 429" in lower or "rate limit" in lower:
raise HTTPException(
status_code=400,
detail=f"Brave API rate limit exceeded: {error_msg}",
raise OnyxError(
OnyxErrorCode.RATE_LIMITED,
f"Brave API rate limit exceeded: {error_msg}",
) from e
raise HTTPException(
status_code=400,
detail=f"Brave API key validation failed: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Brave API key validation failed: {error_msg}",
) from e
logger.info("Web search provider test succeeded for Brave.")

View File

@@ -5,9 +5,10 @@ from typing import Any
import requests
from exa_py import Exa
from exa_py.api import HighlightsContentsOptions
from fastapi import HTTPException
from onyx.connectors.cross_connector_utils.miscellaneous_utils import time_str_to_utc
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.tools.tool_implementations.open_url.models import WebContent
from onyx.tools.tool_implementations.open_url.models import WebContentProvider
from onyx.tools.tool_implementations.web_search.models import (
@@ -167,11 +168,11 @@ class ExaClient(WebSearchProvider, WebContentProvider):
try:
test_results = self.search("test")
if not test_results or not any(result.link for result in test_results):
raise HTTPException(
status_code=400,
detail="API key validation failed: search returned no results.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"API key validation failed: search returned no results.",
)
except HTTPException:
except OnyxError:
raise
except Exception as e:
error_msg = str(e)
@@ -180,13 +181,13 @@ class ExaClient(WebSearchProvider, WebContentProvider):
or "key" in error_msg.lower()
or "auth" in error_msg.lower()
):
raise HTTPException(
status_code=400,
detail=f"Invalid Exa API key: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Invalid Exa API key: {error_msg}",
) from e
raise HTTPException(
status_code=400,
detail=f"Exa API key validation failed: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Exa API key validation failed: {error_msg}",
) from e
logger.info("Web search provider test succeeded for Exa.")

View File

@@ -4,8 +4,9 @@ from datetime import datetime
from typing import Any
import requests
from fastapi import HTTPException
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.tools.tool_implementations.web_search.models import (
WebSearchProvider,
)
@@ -131,11 +132,11 @@ class GooglePSEClient(WebSearchProvider):
try:
test_results = self.search("test")
if not test_results or not any(result.link for result in test_results):
raise HTTPException(
status_code=400,
detail="Google PSE validation failed: search returned no results.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Google PSE validation failed: search returned no results.",
)
except HTTPException:
except OnyxError:
raise
except Exception as e:
error_msg = str(e)
@@ -144,13 +145,13 @@ class GooglePSEClient(WebSearchProvider):
or "key" in error_msg.lower()
or "auth" in error_msg.lower()
):
raise HTTPException(
status_code=400,
detail=f"Invalid Google PSE API key: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Invalid Google PSE API key: {error_msg}",
) from e
raise HTTPException(
status_code=400,
detail=f"Google PSE validation failed: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Google PSE validation failed: {error_msg}",
) from e
logger.info("Web search provider test succeeded for Google PSE.")

View File

@@ -1,6 +1,7 @@
import requests
from fastapi import HTTPException
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.tools.tool_implementations.web_search.models import (
WebSearchProvider,
)
@@ -64,22 +65,20 @@ class SearXNGClient(WebSearchProvider):
f"HTTPError: status_code={status_code}, e.response={e.response.status_code if e.response else None}, error={e}"
)
if status_code == 429:
raise HTTPException(
status_code=400,
detail=(
"This SearXNG instance does not allow API requests. "
"Use a private instance and configure it to allow bots."
),
raise OnyxError(
OnyxErrorCode.CONNECTOR_VALIDATION_FAILED,
"This SearXNG instance does not allow API requests. "
"Use a private instance and configure it to allow bots.",
) from e
elif status_code == 404:
raise HTTPException(
status_code=400,
detail="This SearXNG instance was not found. Please check the URL and try again.",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
"This SearXNG instance was not found. Please check the URL and try again.",
) from e
else:
raise HTTPException(
status_code=400,
detail=f"SearXNG connection failed (status {status_code}): {str(e)}",
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
f"SearXNG connection failed (status {status_code}): {str(e)}",
) from e
# Not a sure way to check if this is a SearXNG instance as opposed to some other website that
@@ -91,9 +90,9 @@ class SearXNGClient(WebSearchProvider):
config.get("brand", {}).get("GIT_URL")
!= "https://github.com/searxng/searxng"
):
raise HTTPException(
status_code=400,
detail="This does not appear to be a SearXNG instance. Please check the URL and try again.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"This does not appear to be a SearXNG instance. Please check the URL and try again.",
)
# Test that JSON mode is enabled by performing a simple search
@@ -122,16 +121,14 @@ class SearXNGClient(WebSearchProvider):
except requests.HTTPError as e:
status_code = e.response.status_code if e.response is not None else None
if status_code == 403:
raise HTTPException(
status_code=400,
detail=(
"Got a 403 response when trying to reach SearXNG. This likely means that "
"JSON format is not enabled on this SearXNG instance. "
"Please enable JSON format in your SearXNG settings.yml file by adding "
"'json' to the 'search.formats' list."
),
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Got a 403 response when trying to reach SearXNG. This likely means that "
"JSON format is not enabled on this SearXNG instance. "
"Please enable JSON format in your SearXNG settings.yml file by adding "
"'json' to the 'search.formats' list.",
) from e
raise HTTPException(
status_code=400,
detail=f"Failed to test search on SearXNG instance (status {status_code}): {str(e)}",
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
f"Failed to test search on SearXNG instance (status {status_code}): {str(e)}",
) from e

View File

@@ -3,9 +3,10 @@ from collections.abc import Sequence
from concurrent.futures import ThreadPoolExecutor
import requests
from fastapi import HTTPException
from onyx.connectors.cross_connector_utils.miscellaneous_utils import time_str_to_utc
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.tools.tool_implementations.open_url.models import WebContent
from onyx.tools.tool_implementations.open_url.models import WebContentProvider
from onyx.tools.tool_implementations.web_search.models import (
@@ -78,11 +79,11 @@ class SerperClient(WebSearchProvider, WebContentProvider):
try:
test_results = self.search("test")
if not test_results or not any(result.link for result in test_results):
raise HTTPException(
status_code=400,
detail="API key validation failed: search returned no results.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"API key validation failed: search returned no results.",
)
except HTTPException:
except OnyxError:
raise
except Exception as e:
error_msg = str(e)
@@ -91,13 +92,13 @@ class SerperClient(WebSearchProvider, WebContentProvider):
or "key" in error_msg.lower()
or "auth" in error_msg.lower()
):
raise HTTPException(
status_code=400,
detail=f"Invalid Serper API key: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Invalid Serper API key: {error_msg}",
) from e
raise HTTPException(
status_code=400,
detail=f"Serper API key validation failed: {error_msg}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Serper API key validation failed: {error_msg}",
) from e
logger.info("Web search provider test succeeded for Serper.")