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

  1. Sanitize the hostname: strip whitespace and disallowed chars before encoding.
  2. Validate each dot-separated label is 1-63 chars and matches RFC 1035 (letters/digits/hyphen).
  3. Use httpx.URL(...) with a pre-encoded punycode host (xn--...) if you manage IDNA yourself.
  4. 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

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


AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04). Data as JSON: /data/errors/f0b3e9492e5ca24d.json. Report an issue: GitHub.