From a23676eecf94b8025b2a817a71358f8e8311fd40 Mon Sep 17 00:00:00 2001 From: Friday Date: Wed, 18 Feb 2026 04:44:07 +0000 Subject: [PATCH] Include header name in UnicodeEncodeError for invalid header values When a header value contains non-ASCII characters that can't be encoded, the error message now includes the header name, making it easier to identify which header is causing the issue. Fixes #3400 Co-Authored-By: Claude Opus 4.6 --- httpx/_models.py | 21 +++++++++++++++++---- tests/models/test_headers.py | 6 ++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index 2cc86321a4..1148e11a23 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -71,7 +71,9 @@ def _normalize_header_key(key: str | bytes, encoding: str | None = None) -> byte return key if isinstance(key, bytes) else key.encode(encoding or "ascii") -def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes: +def _normalize_header_value( + value: str | bytes, encoding: str | None = None, name: str | bytes = "" +) -> bytes: """ Coerce str/bytes into a strictly byte-wise HTTP header value. """ @@ -79,7 +81,18 @@ def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> return value if not isinstance(value, str): raise TypeError(f"Header value must be str or bytes, not {type(value)}") - return value.encode(encoding or "ascii") + try: + return value.encode(encoding or "ascii") + except UnicodeEncodeError as exc: + if name: + raise UnicodeEncodeError( + exc.encoding, + exc.object, + exc.start, + exc.end, + f"{exc.reason} (header: {name!r})", + ) from None + raise def _parse_content_type_charset(content_type: str) -> str | None: @@ -153,12 +166,12 @@ def __init__( elif isinstance(headers, Mapping): for k, v in headers.items(): bytes_key = _normalize_header_key(k, encoding) - bytes_value = _normalize_header_value(v, encoding) + bytes_value = _normalize_header_value(v, encoding, name=k) self._list.append((bytes_key, bytes_key.lower(), bytes_value)) elif headers is not None: for k, v in headers: bytes_key = _normalize_header_key(k, encoding) - bytes_value = _normalize_header_value(v, encoding) + bytes_value = _normalize_header_value(v, encoding, name=k) self._list.append((bytes_key, bytes_key.lower(), bytes_value)) self._encoding = encoding diff --git a/tests/models/test_headers.py b/tests/models/test_headers.py index a87a446784..6e6820b848 100644 --- a/tests/models/test_headers.py +++ b/tests/models/test_headers.py @@ -214,6 +214,12 @@ def test_parse_header_links(value, expected): assert all(link in all_links for link in expected) +def test_header_encoding_error_includes_name(): + # https://github.com/encode/httpx/issues/3400 + with pytest.raises(UnicodeEncodeError, match="header: 'auth'"): + httpx.Headers({"auth": "\u0437\u0434\u0440\u0430\u0432\u0435\u0439"}) + + def test_parse_header_links_no_link(): all_links = httpx.Response(200).links assert all_links == {}