mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-30 01:52:18 +00:00
fix(proxy): percent-encode non-latin1 values in response headers
A model name or deployment string outside latin-1 (e.g. Chinese) crashed every response with UnicodeEncodeError when FastAPI encoded x-litellm-model-name/x-litellm-model-group. get_custom_headers now percent-encodes any header value that cannot be latin-1 encoded. Fixes #39284
This commit is contained in:
parent
2ffe6a1dc8
commit
0847efaf38
4 changed files with 66 additions and 2 deletions
|
|
@ -65,7 +65,10 @@ from litellm.proxy.dd_span_tagger import DDSpanTagger
|
|||
from litellm.proxy.route_llm_request import route_request
|
||||
from litellm.proxy.utils import ProxyLogging, _check_and_merge_model_level_guardrails
|
||||
from litellm.router import Router
|
||||
from litellm.router_utils.add_retry_fallback_headers import get_hidden_params_dict
|
||||
from litellm.router_utils.add_retry_fallback_headers import (
|
||||
get_hidden_params_dict,
|
||||
safe_header_value,
|
||||
)
|
||||
from litellm.router_utils.common_utils import resolve_model_group_alias
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
from litellm.types.router import RouterRateLimitError
|
||||
|
|
@ -1648,7 +1651,11 @@ class ProxyBaseLLMRequestProcessing:
|
|||
headers.update(logging_caching_headers)
|
||||
|
||||
try:
|
||||
return {key: str(value) for key, value in headers.items() if value not in exclude_values}
|
||||
return {
|
||||
key: safe_header_value(str(value))
|
||||
for key, value in headers.items()
|
||||
if value not in exclude_values
|
||||
}
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error("Error setting custom headers: %s", e)
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from typing import Any, Final, Protocol, TypedDict, cast
|
||||
from urllib.parse import quote
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -50,6 +51,21 @@ def prepare_response_for_header_attachment(response: object) -> object | None:
|
|||
return response
|
||||
|
||||
|
||||
def safe_header_value(value: str) -> str:
|
||||
"""
|
||||
HTTP header values must be latin-1 encodable (RFC 7230). Values derived from
|
||||
user-configured strings (e.g. a model name) can contain non-latin-1
|
||||
characters, which crashes response header assembly with a UnicodeEncodeError.
|
||||
Percent-encode such values so they stay ASCII-safe and reversible via
|
||||
``urllib.parse.unquote``, instead of dropping them or crashing the request.
|
||||
"""
|
||||
try:
|
||||
value.encode("latin-1")
|
||||
return value
|
||||
except UnicodeEncodeError:
|
||||
return quote(value, safe="")
|
||||
|
||||
|
||||
def ensure_response_additional_headers(response: object) -> dict[str, object]:
|
||||
hidden_params: Final = get_hidden_params_dict(response, create=isinstance(response, dict))
|
||||
_write_hidden_params(response, hidden_params)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import json
|
|||
from types import SimpleNamespace
|
||||
from typing import AsyncGenerator, Callable, Final, Optional
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from urllib.parse import unquote
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
|
@ -800,6 +801,33 @@ class TestProxyBaseLLMRequestProcessing:
|
|||
assert "x-litellm-response-cost-original" not in headers
|
||||
assert "x-litellm-response-cost-discount-amount" not in headers
|
||||
|
||||
def test_get_custom_headers_percent_encodes_non_latin1_model_name(self):
|
||||
"""
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/39284:
|
||||
a deployment or model-group name outside latin-1 (e.g. Chinese) used to
|
||||
crash header assembly with UnicodeEncodeError when FastAPI/Starlette
|
||||
encoded the response headers.
|
||||
"""
|
||||
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
|
||||
mock_user_api_key_dict.tpm_limit = None
|
||||
mock_user_api_key_dict.rpm_limit = None
|
||||
mock_user_api_key_dict.max_budget = None
|
||||
mock_user_api_key_dict.spend = 0
|
||||
|
||||
headers = ProxyBaseLLMRequestProcessing.get_custom_headers(
|
||||
user_api_key_dict=mock_user_api_key_dict,
|
||||
call_id="test-call-id",
|
||||
model_id="中文模型",
|
||||
)
|
||||
|
||||
encoded_model_id = headers["x-litellm-model-id"]
|
||||
assert encoded_model_id.encode("latin-1")
|
||||
assert unquote(encoded_model_id) == "中文模型"
|
||||
|
||||
# Would raise UnicodeEncodeError before the fix.
|
||||
response = Response(headers=headers)
|
||||
assert response.headers["x-litellm-model-id"] == encoded_model_id
|
||||
|
||||
def test_get_custom_headers_with_margin_info(self):
|
||||
"""
|
||||
Test that margin headers are included when margin is applied.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
from urllib.parse import unquote
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ from litellm.router_utils.add_retry_fallback_headers import (
|
|||
add_retry_headers_to_response,
|
||||
get_fallback_errors_from_headers,
|
||||
get_hidden_params_dict,
|
||||
safe_header_value,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -162,3 +164,14 @@ def test_add_fallback_headers_to_dict_response():
|
|||
|
||||
assert result is response
|
||||
assert response["_hidden_params"]["additional_headers"]["x-litellm-attempted-fallbacks"] == 1
|
||||
|
||||
|
||||
def test_safe_header_value_passes_through_latin1_values():
|
||||
assert safe_header_value("azure/gpt-4o") == "azure/gpt-4o"
|
||||
|
||||
|
||||
def test_safe_header_value_percent_encodes_non_latin1_values():
|
||||
result = safe_header_value("azure/中文模型名稱")
|
||||
|
||||
assert result.encode("latin-1")
|
||||
assert unquote(result) == "azure/中文模型名稱"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue