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 == {}