Skip to content

Commit 80b911e

Browse files
committed
Improve error message for non-ASCII header encoding errors
When headers contain non-ASCII characters, the error message now includes the header name to make debugging easier. Fixes #3400
1 parent def4778 commit 80b911e

2 files changed

Lines changed: 66 additions & 10 deletions

File tree

โ€Žhttpx/_models.pyโ€Ž

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,68 @@ def _is_known_encoding(encoding: str) -> bool:
6464
return True
6565

6666

67-
def _normalize_header_key(key: str | bytes, encoding: str | None = None) -> bytes:
67+
def _normalize_header_key(
68+
key: str | bytes,
69+
encoding: str | None = None,
70+
header_name: str | bytes | None = None,
71+
) -> bytes:
6872
"""
6973
Coerce str/bytes into a strictly byte-wise HTTP header key.
7074
"""
71-
return key if isinstance(key, bytes) else key.encode(encoding or "ascii")
72-
73-
74-
def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes:
75+
if isinstance(key, bytes):
76+
return key
77+
try:
78+
return key.encode(encoding or "ascii")
79+
except UnicodeEncodeError as exc:
80+
if header_name:
81+
name_str = (
82+
header_name
83+
if isinstance(header_name, str)
84+
else header_name.decode("ascii", errors="replace")
85+
)
86+
header_info = f" '{name_str}'"
87+
else:
88+
header_info = ""
89+
raise UnicodeEncodeError(
90+
exc.encoding,
91+
exc.object,
92+
exc.start,
93+
exc.end,
94+
f"Header name{header_info} contains non-ASCII characters",
95+
) from exc
96+
97+
98+
def _normalize_header_value(
99+
value: str | bytes,
100+
encoding: str | None = None,
101+
header_name: str | bytes | None = None,
102+
) -> bytes:
75103
"""
76104
Coerce str/bytes into a strictly byte-wise HTTP header value.
77105
"""
78106
if isinstance(value, bytes):
79107
return value
80108
if not isinstance(value, str):
81109
raise TypeError(f"Header value must be str or bytes, not {type(value)}")
82-
return value.encode(encoding or "ascii")
110+
try:
111+
return value.encode(encoding or "ascii")
112+
except UnicodeEncodeError as exc:
113+
if header_name:
114+
name_str = (
115+
header_name
116+
if isinstance(header_name, str)
117+
else header_name.decode("ascii", errors="replace")
118+
)
119+
header_info = f" '{name_str}'"
120+
else:
121+
header_info = ""
122+
raise UnicodeEncodeError(
123+
exc.encoding,
124+
exc.object,
125+
exc.start,
126+
exc.end,
127+
f"Header{header_info} value contains non-ASCII characters",
128+
) from exc
83129

84130

85131
def _parse_content_type_charset(content_type: str) -> str | None:
@@ -152,13 +198,13 @@ def __init__(
152198
self._list = list(headers._list)
153199
elif isinstance(headers, Mapping):
154200
for k, v in headers.items():
155-
bytes_key = _normalize_header_key(k, encoding)
156-
bytes_value = _normalize_header_value(v, encoding)
201+
bytes_key = _normalize_header_key(k, encoding, header_name=k)
202+
bytes_value = _normalize_header_value(v, encoding, header_name=k)
157203
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
158204
elif headers is not None:
159205
for k, v in headers:
160-
bytes_key = _normalize_header_key(k, encoding)
161-
bytes_value = _normalize_header_value(v, encoding)
206+
bytes_key = _normalize_header_key(k, encoding, header_name=k)
207+
bytes_value = _normalize_header_value(v, encoding, header_name=k)
162208
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
163209

164210
self._encoding = encoding

โ€Žtests/models/test_headers.pyโ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,13 @@ def test_parse_header_links(value, expected):
217217
def test_parse_header_links_no_link():
218218
all_links = httpx.Response(200).links
219219
assert all_links == {}
220+
221+
222+
def test_header_encoding_error_mentions_header_name():
223+
with pytest.raises(UnicodeEncodeError, match="Header 'auth' value"):
224+
httpx.Headers({"auth": "์•ˆ๋…•ํ•˜์„ธ์š”"})
225+
226+
227+
def test_header_key_encoding_error_mentions_header_name():
228+
with pytest.raises(UnicodeEncodeError, match="Header name 'ํ—ค๋”'"):
229+
httpx.Headers({"ํ—ค๋”": "value"})

0 commit comments

Comments
ย (0)