encode/httpx · error · InvalidURL

Invalid non-printable ASCII character in URL, {char!r} at po

Error message

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

What it means

Raised by urlparse when the raw URL string contains any ASCII character that is not printable (e.g. \t, \r, \n, NUL, or other C0 controls). Such characters are forbidden by the URL grammar and also enable request-smuggling / header-injection attacks, so httpx rejects them up front as InvalidURL.

Source

Thrown at httpx/_urlparse.py:229


def urlparse(url: str = "", **kwargs: str | None) -> ParseResult:
    # Initial basic checks on allowable URLs.
    # ---------------------------------------

    # Hard limit the maximum allowable URL length.
    if len(url) > MAX_URL_LENGTH:
        raise InvalidURL("URL too long")

    # If a URL 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 url):
        char = next(char for char in url if char.isascii() and not char.isprintable())
        idx = url.find(char)
        error = (
            f"Invalid non-printable ASCII character in URL, {char!r} at position {idx}."
        )
        raise InvalidURL(error)

    # Some keyword arguments require special handling.
    # ------------------------------------------------

    # Coerce "port" to a string, if it is provided as an integer.
    if "port" in kwargs:
        port = kwargs["port"]
        kwargs["port"] = str(port) if isinstance(port, int) else port

    # Replace "netloc" with "host and "port".
    if "netloc" in kwargs:
        netloc = kwargs.pop("netloc") or ""
        kwargs["host"], _, kwargs["port"] = netloc.partition(":")

    # Replace "username" and/or "password" with "userinfo".
    if "username" in kwargs or "password" in kwargs:
        username = quote(kwargs.pop("username", "") or "", safe=USERNAME_SAFE)
        password = quote(kwargs.pop("password", "") or "", safe=PASSWORD_SAFE)

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Sanitize user input: strip/replace control chars before building the URL (e.g. url.translate({ord(c): None for c in '\r\n\t'})).
  2. Pass components separately with httpx.URL(scheme=..., host=..., path=...) so httpx can percent-encode safely.
  3. Percent-encode path/query values with urllib.parse.quote rather than concatenating raw strings.
  4. Reject inputs containing control characters at the validation boundary.

Example fix

// before
url = "https://api.example.com/" + user_path  # user_path = "foo\nHost: evil\r\n"
client.get(url)  # InvalidURL: non-printable ASCII

// after
from urllib.parse import quote
url = "https://api.example.com/" + quote(user_path, safe="")
Defensive patterns

Strategy: validation

Validate before calling

import unicodedata

def sanitize_url(url: str) -> str:
    # Remove ASCII C0 controls and DEL except those already percent-encoded
    return "".join(
        ch for ch in url
        if not (ch.isascii() and not ch.isprintable())
    )

client.get(sanitize_url(raw_url))

Type guard

def url_has_no_controls(url: str) -> bool:
    return not any(ch.isascii() and not ch.isprintable() for ch in url)

Try / catch

from httpx import InvalidURL

try:
    client.get(url)
except InvalidURL as e:
    if "non-printable" in str(e):
        from urllib.parse import quote
        url = quote(url, safe=":/?#[]@!$&'()*+,;=")
        client.get(url)
    else:
        raise

Prevention

When it happens

Trigger: Building a URL by string-concatenating untrusted input that contains newlines (e.g. a user-supplied path with '\n'); reading URLs from a CSV/log where line breaks leak in; copy-paste introducing a stray tab.

Common situations: CRLF injection attempts via user input; multi-line values accidentally concatenated into a URL; log lines containing carriage returns being used as URLs.

Related errors


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