fix(proxy): also percent-encode a literal % in header values

Greptile flagged that a latin-1-safe value containing a literal "%"
(e.g. "model%20name") passed through unchanged, making it
indistinguishable from a genuinely percent-encoded value once a
caller starts unquoting these headers unconditionally.
This commit is contained in:
feng.tsai 2026-09-03 02:54:40 +08:00
parent 45e102785e
commit 6cad229179
2 changed files with 31 additions and 17 deletions

View file

@ -58,12 +58,24 @@ def safe_header_value(value: str) -> str:
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.
A value that is already latin-1 safe but contains a literal ``%`` is also
percent-encoded: left alone, it would be indistinguishable from a genuinely
encoded value once mixed with the encoded case, so a caller applying
``unquote`` unconditionally would silently decode the wrong string.
"""
is_latin1_safe: Final = _is_latin1_safe(value)
if is_latin1_safe and "%" not in value:
return value
return quote(value, safe="")
def _is_latin1_safe(value: str) -> bool:
try:
value.encode("latin-1")
return value
return True
except UnicodeEncodeError:
return quote(value, safe="")
return False
def ensure_response_additional_headers(response: object) -> dict[str, object]:

View file

@ -50,15 +50,8 @@ def test_add_fallback_headers_serializes_fallback_errors():
)
assert result is response
assert response._hidden_params["additional_headers"][
"x-litellm-attempted-fallbacks"
] == 1
assert (
json.loads(
response._hidden_params["additional_headers"]["x-litellm-fallback-errors"]
)
== fallback_errors
)
assert response._hidden_params["additional_headers"]["x-litellm-attempted-fallbacks"] == 1
assert json.loads(response._hidden_params["additional_headers"]["x-litellm-fallback-errors"]) == fallback_errors
def test_add_retry_headers_to_streaming_wrapper():
@ -84,9 +77,7 @@ def test_get_hidden_params_dict_with_pydantic_model_hidden_params():
class Response:
def __init__(self):
self._hidden_params = InnerHiddenParams(
additional_headers={"x-custom": "value"}
)
self._hidden_params = InnerHiddenParams(additional_headers={"x-custom": "value"})
result = get_hidden_params_dict(Response())
assert result == {"additional_headers": {"x-custom": "value"}}
@ -133,9 +124,7 @@ def test_get_fallback_errors_from_headers_existing_list_passthrough():
def test_get_fallback_errors_from_headers_invalid_json_returns_empty():
result = get_fallback_errors_from_headers(
{"x-litellm-fallback-errors": "not-valid-json-{"}
)
result = get_fallback_errors_from_headers({"x-litellm-fallback-errors": "not-valid-json-{"})
assert result == []
@ -175,3 +164,16 @@ def test_safe_header_value_percent_encodes_non_latin1_values():
assert result.encode("latin-1")
assert unquote(result) == "azure/中文模型名稱"
def test_safe_header_value_escapes_a_literal_percent_sign_too():
"""
A latin-1-safe value that happens to contain a literal "%" (e.g.
"model%20name") must not pass through unchanged: it would then be
indistinguishable from a genuinely percent-encoded value, and a caller
that always unquotes would silently decode it into the wrong string.
"""
result = safe_header_value("model%20name")
assert "%" not in result.replace("%25", "")
assert unquote(result) == "model%20name"