encode/httpx · error · InvalidURL
Invalid IDNA hostname: {host!r}
Error message
Invalid IDNA hostname: {host!r} What it means
Raised by encode_host for non-ASCII hostnames when idna.encode() raises IDNAError. httpx uses the idna library to convert internationalized domain names to punycode; inputs that violate IDNA 2008 (e.g. illegal characters, overly long labels > 63 chars, empty labels) cannot be encoded and are rejected.
Source
Thrown at httpx/_urlparse.py:392
ipaddress.IPv6Address(host[1:-1])
except ipaddress.AddressValueError:
raise InvalidURL(f"Invalid IPv6 address: {host!r}")
return host[1:-1]
elif host.isascii():
# Regular ASCII hostnames
#
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
#
# reg-name = *( unreserved / pct-encoded / sub-delims )
WHATWG_SAFE = '"`{}%|\\'
return quote(host.lower(), safe=SUB_DELIMS + WHATWG_SAFE)
# IDNA hostnames
try:
return idna.encode(host.lower()).decode("ascii")
except idna.IDNAError:
raise InvalidURL(f"Invalid IDNA hostname: {host!r}")
def normalize_port(port: str | int | None, scheme: str) -> int | None:
# From https://tools.ietf.org/html/rfc3986#section-3.2.3
#
# "A scheme may define a default port. For example, the "http" scheme
# defines a default port of "80", corresponding to its reserved TCP
# port number. The type of port designated by the port number (e.g.,
# TCP, UDP, SCTP) is defined by the URI scheme. URI producers and
# normalizers should omit the port component and its ":" delimiter if
# port is empty or if its value would be the same as that of the
# scheme's default."
if port is None or port == "":
return None
try:
port_as_int = int(port)
except ValueError:View on GitHub (pinned to b5addb64f0)
Solutions
- Sanitize the hostname: strip whitespace and disallowed chars before encoding.
- Validate each dot-separated label is 1-63 chars and matches RFC 1035 (letters/digits/hyphen).
- Use httpx.URL(...) with a pre-encoded punycode host (xn--...) if you manage IDNA yourself.
- Fall back to a known-good hostname when user input is unparseable.
Example fix
// before
url = httpx.URL("http://exa mple.com/") # Invalid IDNA hostname
// after
host = "exa mple.com".replace(" ", "")
url = httpx.URL(f"http://{host}/") Defensive patterns
Strategy: validation
Validate before calling
import re
import idna
def safe_host(host: str) -> str:
host = host.strip().lower()
for label in host.split("."):
if not label or len(label) > 63:
raise ValueError(f"Bad host label in {host!r}")
try:
idna.encode(host)
except idna.IDNAError as e:
raise ValueError(f"Invalid IDNA hostname {host!r}") from e
return host
url = httpx.URL(host=safe_host(user_host)) Type guard
import idna
def is_valid_host(host: str) -> bool:
try:
idna.encode(host)
return True
except idna.IDNAError:
return False Try / catch
from httpx import InvalidURL
try:
url = httpx.URL(f"http://{host}/")
except InvalidURL as e:
if "Invalid IDNA" in str(e):
# fall back to a punycode-encoded host or reject
raise ValueError(f"Unusable hostname {host!r}") from e
raise Prevention
- Strip whitespace and disallowed punctuation from hostnames.
- Validate each label is 1-63 chars and letters/digits/hyphen.
- Reject underscores in hostnames unless you control DNS.
When it happens
Trigger: httpx.URL('http://exa mple.com/'), httpx.URL(host='xn--invalid'), a host with a label longer than 63 characters, or a host containing a character idna rejects (e.g. underscore in some positions).
Common situations: User-typed URLs with spaces or underscores; copy-paste introducing zero-width characters; very long single-label hostnames; mixing valid IDNA with disallowed punctuation.
Related errors
- URL too long
- Invalid non-printable ASCII character in URL, {char!r} at po
- URL component '{key}' too long
- Invalid non-printable ASCII character in URL {key} component
- Invalid URL component '{key}'
AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04).
Data as JSON: /data/errors/f0b3e9492e5ca24d.json.
Report an issue: GitHub.