Skip to content

Commit ee1a3cc

Browse files
Merge pull request #4 from AndreyLebedev345/test-branch
Test branch
2 parents 435e1da + 2855333 commit ee1a3cc

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

httpx/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ._transports import *
1111
from ._types import *
1212
from ._urls import *
13+
from ._utils import normalize_header_key
1314

1415
try:
1516
from ._main import main
@@ -63,6 +64,7 @@ def main() -> None: # type: ignore
6364
"MockTransport",
6465
"NetRCAuth",
6566
"NetworkError",
67+
"normalize_header_key",
6668
"options",
6769
"patch",
6870
"PoolTimeout",

httpx/_utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ def __eq__(self, other: typing.Any) -> bool:
227227

228228

229229
def is_ipv4_hostname(hostname: str) -> bool:
230+
"""
231+
Check if the given hostname is a valid IPv4 address.
232+
Supports CIDR notation by checking only the address part.
233+
"""
230234
try:
231235
ipaddress.IPv4Address(hostname.split("/")[0])
232236
except Exception:
@@ -235,8 +239,53 @@ def is_ipv4_hostname(hostname: str) -> bool:
235239

236240

237241
def is_ipv6_hostname(hostname: str) -> bool:
242+
"""
243+
Check if the given hostname is a valid IPv6 address.
244+
Supports CIDR notation by checking only the address part.
245+
"""
238246
try:
239247
ipaddress.IPv6Address(hostname.split("/")[0])
240248
except Exception:
241249
return False
242250
return True
251+
252+
253+
def is_ip_address(hostname: str) -> bool:
254+
"""
255+
Check if the given hostname is a valid IP address (either IPv4 or IPv6).
256+
Supports CIDR notation by checking only the address part.
257+
"""
258+
return is_ipv4_hostname(hostname) or is_ipv6_hostname(hostname)
259+
260+
261+
def normalize_header_key(key: str, *, preserve_case: bool = False) -> str:
262+
"""
263+
Normalize HTTP header keys for consistent comparison and storage.
264+
265+
By default, converts header keys to lowercase following HTTP/2 conventions.
266+
Can optionally preserve the original case for HTTP/1.1 compatibility.
267+
268+
Args:
269+
key: The header key to normalize
270+
preserve_case: If True, preserve the original case. If False (default),
271+
convert to lowercase.
272+
273+
Returns:
274+
The normalized header key as a string
275+
276+
Examples:
277+
>>> normalize_header_key("Content-Type")
278+
'content-type'
279+
>>> normalize_header_key("Content-Type", preserve_case=True)
280+
'Content-Type'
281+
>>> normalize_header_key("X-Custom-Header")
282+
'x-custom-header'
283+
284+
Note:
285+
This function is useful when working with HTTP headers across different
286+
protocol versions. HTTP/2 requires lowercase header names, while HTTP/1.1
287+
traditionally uses title-case headers (though comparison is case-insensitive).
288+
"""
289+
if preserve_case:
290+
return key.strip()
291+
return key.strip().lower()

0 commit comments

Comments
 (0)