{"id":"4eb9c997c000d4f3","repo":"encode/httpx","slug":"invalid-non-printable-ascii-character-in-url-cha","errorCode":null,"errorMessage":"Invalid non-printable ASCII character in URL, {char!r} at position {idx}.","messagePattern":"Invalid non-printable ASCII character in URL, (.+?) at position (.+?)\\.","errorType":"validation","errorClass":"InvalidURL","httpStatus":null,"severity":"error","filePath":"httpx/_urlparse.py","lineNumber":229,"sourceCode":"\n\ndef urlparse(url: str = \"\", **kwargs: str | None) -> ParseResult:\n    # Initial basic checks on allowable URLs.\n    # ---------------------------------------\n\n    # Hard limit the maximum allowable URL length.\n    if len(url) > MAX_URL_LENGTH:\n        raise InvalidURL(\"URL too long\")\n\n    # If a URL includes any ASCII control characters including \\t, \\r, \\n,\n    # then treat it as invalid.\n    if any(char.isascii() and not char.isprintable() for char in url):\n        char = next(char for char in url if char.isascii() and not char.isprintable())\n        idx = url.find(char)\n        error = (\n            f\"Invalid non-printable ASCII character in URL, {char!r} at position {idx}.\"\n        )\n        raise InvalidURL(error)\n\n    # Some keyword arguments require special handling.\n    # ------------------------------------------------\n\n    # Coerce \"port\" to a string, if it is provided as an integer.\n    if \"port\" in kwargs:\n        port = kwargs[\"port\"]\n        kwargs[\"port\"] = str(port) if isinstance(port, int) else port\n\n    # Replace \"netloc\" with \"host and \"port\".\n    if \"netloc\" in kwargs:\n        netloc = kwargs.pop(\"netloc\") or \"\"\n        kwargs[\"host\"], _, kwargs[\"port\"] = netloc.partition(\":\")\n\n    # Replace \"username\" and/or \"password\" with \"userinfo\".\n    if \"username\" in kwargs or \"password\" in kwargs:\n        username = quote(kwargs.pop(\"username\", \"\") or \"\", safe=USERNAME_SAFE)\n        password = quote(kwargs.pop(\"password\", \"\") or \"\", safe=PASSWORD_SAFE)","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/encode/httpx/blob/b5addb64f0161ff6bfe94c124ef76f6a1fba5254/httpx/_urlparse.py#L211-L247","documentation":"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.","triggerScenarios":"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.","commonSituations":"CRLF injection attempts via user input; multi-line values accidentally concatenated into a URL; log lines containing carriage returns being used as URLs.","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."],"exampleFix":"// before\nurl = \"https://api.example.com/\" + user_path  # user_path = \"foo\\nHost: evil\\r\\n\"\nclient.get(url)  # InvalidURL: non-printable ASCII\n\n// after\nfrom urllib.parse import quote\nurl = \"https://api.example.com/\" + quote(user_path, safe=\"\")","handlingStrategy":"validation","validationCode":"import unicodedata\n\ndef sanitize_url(url: str) -> str:\n    # Remove ASCII C0 controls and DEL except those already percent-encoded\n    return \"\".join(\n        ch for ch in url\n        if not (ch.isascii() and not ch.isprintable())\n    )\n\nclient.get(sanitize_url(raw_url))","typeGuard":"def url_has_no_controls(url: str) -> bool:\n    return not any(ch.isascii() and not ch.isprintable() for ch in url)","tryCatchPattern":"from httpx import InvalidURL\n\ntry:\n    client.get(url)\nexcept InvalidURL as e:\n    if \"non-printable\" in str(e):\n        from urllib.parse import quote\n        url = quote(url, safe=\":/?#[]@!$&'()*+,;=\")\n        client.get(url)\n    else:\n        raise","preventionTips":["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."],"tags":["url","security","injection","validation","sanitization"],"analyzedSha":"b5addb64f0161ff6bfe94c124ef76f6a1fba5254","analyzedAt":"2026-08-04T19:32:56.768Z","schemaVersion":2}