encode/httpx · error · InvalidURL

Invalid non-printable ASCII character in URL {key} component

Error message

Invalid non-printable ASCII character in URL {key} component, {char!r} at position {idx}.

What it means

Component-level analogue of error 66: raised during the kwargs validation loop when any individual URL component value (e.g. host, path, query) contains a non-printable ASCII character. Protects against CRLF/control-char injection through individual components the same way the whole-URL check does.

Source

Thrown at httpx/_urlparse.py:282

    # -------------------------------------------------------------

    for key, value in kwargs.items():
        if value is not None:
            if len(value) > MAX_URL_LENGTH:
                raise InvalidURL(f"URL component '{key}' too long")

            # If a component includes any ASCII control characters including \t, \r, \n,
            # then treat it as invalid.
            if any(char.isascii() and not char.isprintable() for char in value):
                char = next(
                    char for char in value if char.isascii() and not char.isprintable()
                )
                idx = value.find(char)
                error = (
                    f"Invalid non-printable ASCII character in URL {key} component, "
                    f"{char!r} at position {idx}."
                )
                raise InvalidURL(error)

            # Ensure that keyword arguments match as a valid regex.
            if not COMPONENT_REGEX[key].fullmatch(value):
                raise InvalidURL(f"Invalid URL component '{key}'")

    # The URL_REGEX will always match, but may have empty components.
    url_match = URL_REGEX.match(url)
    assert url_match is not None
    url_dict = url_match.groupdict()

    # * 'scheme', 'authority', and 'path' may be empty strings.
    # * 'query' may be 'None', indicating no trailing "?" portion.
    #   Any string including the empty string, indicates a trailing "?".
    # * 'fragment' may be 'None', indicating no trailing "#" portion.
    #   Any string including the empty string, indicates a trailing "#".
    scheme = kwargs.get("scheme", url_dict["scheme"]) or ""
    authority = kwargs.get("authority", url_dict["authority"]) or ""
    path = kwargs.get("path", url_dict["path"]) or ""

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Percent-encode the component value with urllib.parse.quote before passing it.
  2. Strip control characters: value = ''.join(c for c in value if c.isprintable()).
  3. Reject inputs with control chars at the validation boundary.
  4. Prefer building the full URL string with safe concatenation rather than passing untrusted kwargs.

Example fix

// before
url = httpx.URL(scheme="https", host=user_host)  # user_host = "evil\r\n"

// after
from urllib.parse import quote
url = httpx.URL(scheme="https", host=quote(user_host, safe=""))
Defensive patterns

Strategy: validation

Validate before calling

def clean_component(value: str) -> str:
    return "".join(c for c in value if not (c.isascii() and not c.isprintable()))

url = httpx.URL(host=clean_component(user_host))

Type guard

def component_is_clean(value: str) -> bool:
    return not any(c.isascii() and not c.isprintable() for c in value)

Try / catch

from httpx import InvalidURL

try:
    url = httpx.URL(host=raw_host)
except InvalidURL as e:
    if "non-printable" in str(e):
        from urllib.parse import quote
        url = httpx.URL(host=quote(raw_host, safe=""))
    else:
        raise

Prevention

When it happens

Trigger: httpx.URL(host=user_input) where user_input contains '\n'; passing a path kwarg with a tab character; query kwarg containing a raw carriage return.

Common situations: User-supplied hostname/path that contains a newline; reading components from a malformed source file; building host from a multi-line env var.

Related errors


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