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
- Sanitize user input: strip/replace control chars before building the URL (e.g. url.translate({ord(c): None for c in '\r\n\t'})).
- Pass components separately with httpx.URL(scheme=..., host=..., path=...) so httpx can percent-encode safely.
- Percent-encode path/query values with urllib.parse.quote rather than concatenating raw strings.
- 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
- Treat all user input as untrusted; strip control characters at the boundary.
- Prefer urllib.parse.quote on dynamic path/query segments over string concatenation.
- Reject CR/LF in any value used to build a URL.
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
- Invalid non-printable ASCII character in URL {key} component
- URL too long
- URL component '{key}' too long
- Invalid URL component '{key}'
- Invalid IPv4 address: {host!r}
AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04).
Data as JSON: /data/errors/4eb9c997c000d4f3.json.
Report an issue: GitHub.