fix(containers): surface provider errors from container file content endpoint (#37737)

The generic container handler returned response.content for endpoints marked
returns_binary before it ran any status or error check, so a non-2xx answer
from the provider was handed back to the caller as raw bytes. Asking for the
content of a container file that does not exist returned the provider's 404
error body as an opaque payload instead of raising.

Move the check ahead of the binary short-circuit and apply it to every
container file endpoint, falling back to the response text when the error body
is not JSON.
This commit is contained in:
yuneng-jiang 2026-08-20 16:46:26 -07:00 committed by GitHub
parent cb89c7aa8f
commit 2a863f8bdd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 161 additions and 42 deletions

View file

@ -39,6 +39,10 @@ RESPONSE_TYPES: Final[dict[str, type]] = {
"DeleteContainerFileResponse": DeleteContainerFileResponse,
}
ContainerEndpointResponse = (
ContainerFileListResponse | ContainerFileObject | DeleteContainerFileResponse | bytes | dict[str, object]
)
def _load_endpoints_config() -> dict:
"""Load the endpoints configuration from JSON file."""
@ -101,6 +105,51 @@ def _build_query_params(
return params
def _error_message_from_response(response: httpx.Response) -> str:
try:
body: Final = response.json()
except ValueError:
return response.text
if isinstance(body, dict) and isinstance(body.get("error"), dict):
message: Final = body["error"].get("message")
if isinstance(message, str):
return message
return response.text
def _transform_response(
response: httpx.Response,
returns_binary: bool,
response_type_name: str,
) -> ContainerEndpointResponse:
from litellm.llms.base_llm.chat.transformation import BaseLLMException
if httpx.codes.is_error(response.status_code):
raise BaseLLMException(
status_code=response.status_code,
message=_error_message_from_response(response),
headers=dict(response.headers),
)
if returns_binary:
return response.content
response_json: Final = response.json()
if "error" in response_json:
raise BaseLLMException(
status_code=response.status_code,
message=response_json.get("error", {}).get("message", str(response_json)),
headers=dict(response.headers),
)
response_type: Final = RESPONSE_TYPES.get(response_type_name)
if response_type:
return response_type(**response_json)
return response_json
def _prepare_multipart_file_upload(
file: Any,
headers: dict[str, Any],
@ -270,27 +319,11 @@ class GenericContainerHandler:
else:
raise ValueError(f"Unsupported HTTP method: {method}")
# For binary responses, return raw content
if returns_binary:
return response.content
# Check for error response
response_json: Final = response.json()
if "error" in response_json:
from litellm.llms.base_llm.chat.transformation import BaseLLMException
error_msg: Final = response_json.get("error", {}).get("message", str(response_json))
raise BaseLLMException(
status_code=response.status_code,
message=error_msg,
headers=dict(response.headers),
)
# Parse response
response_type: Final = RESPONSE_TYPES.get(endpoint_config["response_type"])
if response_type:
return response_type(**response_json)
return response_json
return _transform_response(
response=response,
returns_binary=returns_binary,
response_type_name=endpoint_config["response_type"],
)
except Exception as e:
raise e
@ -378,27 +411,11 @@ class GenericContainerHandler:
else:
raise ValueError(f"Unsupported HTTP method: {method}")
# For binary responses, return raw content
if returns_binary:
return response.content
# Check for error response
response_json: Final = response.json()
if "error" in response_json:
from litellm.llms.base_llm.chat.transformation import BaseLLMException
error_msg: Final = response_json.get("error", {}).get("message", str(response_json))
raise BaseLLMException(
status_code=response.status_code,
message=error_msg,
headers=dict(response.headers),
)
# Parse response
response_type: Final = RESPONSE_TYPES.get(endpoint_config["response_type"])
if response_type:
return response_type(**response_json)
return response_json
return _transform_response(
response=response,
returns_binary=returns_binary,
response_type_name=endpoint_config["response_type"],
)
except Exception as e:
raise e

View file

@ -0,0 +1,102 @@
from unittest.mock import MagicMock
import httpx
import pytest
import litellm
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.custom_httpx.container_handler import generic_container_handler
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager
FILE_NOT_FOUND_BODY = {
"error": {
"message": "File not found.",
"type": "invalid_request_error",
"param": None,
"code": None,
}
}
def _sync_client(response: httpx.Response) -> HTTPHandler:
handler = HTTPHandler()
handler.client = httpx.Client(transport=httpx.MockTransport(lambda _request: response))
return handler
def _async_client(response: httpx.Response) -> AsyncHTTPHandler:
handler = AsyncHTTPHandler()
handler.client = httpx.AsyncClient(transport=httpx.MockTransport(lambda _request: response))
return handler
def _handle(endpoint_name: str, client, **overrides):
return generic_container_handler.handle(
endpoint_name=endpoint_name,
container_provider_config=ProviderConfigManager.get_provider_container_config(
provider=litellm.LlmProviders.OPENAI
),
litellm_params=GenericLiteLLMParams(api_key="sk-test"),
logging_obj=MagicMock(),
client=client,
container_id="cntr_real",
file_id="cfile_nonexistent",
**overrides,
)
def test_binary_endpoint_raises_on_error_status():
with pytest.raises(BaseLLMException) as exc_info:
_handle(
"retrieve_container_file_content",
_sync_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
)
assert exc_info.value.status_code == 404
assert exc_info.value.message == "File not found."
@pytest.mark.asyncio
async def test_async_binary_endpoint_raises_on_error_status():
with pytest.raises(BaseLLMException) as exc_info:
await _handle(
"aretrieve_container_file_content",
_async_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
_is_async=True,
)
assert exc_info.value.status_code == 404
assert exc_info.value.message == "File not found."
def test_binary_endpoint_returns_raw_content_on_success():
content = _handle(
"retrieve_container_file_content",
_sync_client(httpx.Response(200, content=b"\x00binary-payload")),
)
assert content == b"\x00binary-payload"
def test_error_status_with_non_json_body_surfaces_response_text():
with pytest.raises(BaseLLMException) as exc_info:
_handle(
"retrieve_container_file_content",
_sync_client(httpx.Response(502, content=b"<html>bad gateway</html>")),
)
assert exc_info.value.status_code == 502
assert exc_info.value.message == "<html>bad gateway</html>"
def test_json_endpoint_still_raises_provider_error_message():
with pytest.raises(BaseLLMException) as exc_info:
_handle(
"retrieve_container_file",
_sync_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
)
assert exc_info.value.status_code == 404
assert exc_info.value.message == "File not found."