mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-28 01:32:17 +00:00
fix(searxng): preserve upstream HTTP errors instead of returning empty results
The search adapter built a SearchResponse from raw_response.json() without
checking the status code. A 429 or 503 from the SearXNG instance has no
"results" key, so response_json.get("results", []) produced an empty list and
the caller received a successful response with zero results.
That makes a retryable provider failure indistinguishable from a query that
genuinely matched nothing, so fallback between configured search tools never
fires. The HTTP handlers for this provider do not call raise_for_status either,
so no other layer inspects the status.
Raise through the provider's own error class so the existing retry and fallback
machinery classifies it. The upstream body is deliberately not included in the
message, because it can echo the original query.
Tests live under tests/test_litellm/, mirroring the module path, because that
is the tree the unit-test workflow executes — tests/search_tests/ is not run by
any workflow, so coverage there would not reach the changed lines.
This commit is contained in:
parent
bd7e9c1997
commit
acf6a72ada
2 changed files with 83 additions and 0 deletions
|
|
@ -204,6 +204,17 @@ class SearXNGSearchConfig(BaseSearchConfig):
|
|||
Returns:
|
||||
SearchResponse with standardized format
|
||||
"""
|
||||
if not 200 <= raw_response.status_code < 300:
|
||||
# The HTTP handlers for this provider do not call raise_for_status,
|
||||
# so an upstream 429/503 would otherwise be parsed as a successful
|
||||
# response with zero results. The upstream body is deliberately not
|
||||
# echoed, because it can contain the original query.
|
||||
raise self.get_error_class(
|
||||
error_message=(f"SearXNG search provider returned HTTP {raw_response.status_code}"),
|
||||
status_code=raw_response.status_code,
|
||||
headers=dict(raw_response.headers), # mutable-ok: get_error_class takes a plain dict
|
||||
)
|
||||
|
||||
response_json: Final = raw_response.json()
|
||||
|
||||
# Transform results to SearchResult objects
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
"""
|
||||
Regression tests for HTTP status handling in ``SearXNGSearchConfig``.
|
||||
|
||||
The adapter used to build a ``SearchResponse`` from ``raw_response.json()``
|
||||
without inspecting the status code. A 429 or 503 has no ``results`` key, so the
|
||||
caller received a successful response with zero results — indistinguishable
|
||||
from a query that genuinely matched nothing, which meant fallback between
|
||||
configured search tools never fired.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.searxng.search.transformation import SearXNGSearchConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config() -> SearXNGSearchConfig:
|
||||
return SearXNGSearchConfig()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logging_obj() -> MagicMock:
|
||||
return MagicMock()
|
||||
|
||||
|
||||
def _response(status_code: int, payload: dict) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
status_code=status_code,
|
||||
json=payload,
|
||||
request=httpx.Request("GET", "https://searxng.example.com/search"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status_code", [429, 503])
|
||||
def test_retryable_status_is_raised_not_parsed_as_empty(
|
||||
config: SearXNGSearchConfig, logging_obj: MagicMock, status_code: int
|
||||
) -> None:
|
||||
"""A rate-limited or unavailable instance must surface as an error."""
|
||||
with pytest.raises(BaseLLMException) as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=_response(status_code, {"error": "upstream unavailable"}),
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == status_code
|
||||
assert str(status_code) in str(exc_info.value)
|
||||
|
||||
|
||||
def test_error_message_does_not_echo_upstream_body(config: SearXNGSearchConfig, logging_obj: MagicMock) -> None:
|
||||
"""The upstream body can echo the original query; keep it out of the error."""
|
||||
with pytest.raises(BaseLLMException) as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=_response(500, {"error": "failed searching for confidential-term"}),
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
|
||||
assert "confidential-term" not in str(exc_info.value)
|
||||
|
||||
|
||||
def test_success_status_still_parses_normally(config: SearXNGSearchConfig, logging_obj: MagicMock) -> None:
|
||||
"""2xx behaviour is unchanged."""
|
||||
response = config.transform_search_response(
|
||||
raw_response=_response(200, {"results": [{"title": "Still works", "url": "https://example.com"}]}),
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
|
||||
assert len(response.results) == 1
|
||||
assert response.results[0].title == "Still works"
|
||||
Loading…
Add table
Reference in a new issue