psf/requests · error · InvalidURL
Invalid percent-escape sequence
Error message
Invalid percent-escape sequence: '{h}' What it means
Raised by unquote_unreserved() when a percent-escape sequence in a URL contains characters that pass isalnum() but fail int(..., 16) conversion. This is extremely rare since isalnum + length-2 usually implies valid hex, but certain Unicode characters (e.g., non-ASCII letters) can be alphanumeric without being valid hexadecimal digits.
Solutions
- Sanitize/validate URLs before passing to requests: check for valid percent-encoding with a regex.
- Use urllib.parse.quote on user-supplied path components to ensure proper encoding.
- Catch requests.exceptions.InvalidURL and fall back to a quoted version of the URL.
Example fix
# before
requests.get('https://example.com/path%ZZbad') # potential InvalidURL
# after — sanitize the URL first
from urllib.parse import quote, unquote
safe_url = quote(unquote(url), safe=':/?&=%')
requests.get(safe_url) Defensive patterns
Strategy: validation
Validate before calling
import re
if re.search(r'%[^0-9A-Fa-f]', url):
from urllib.parse import quote, unquote
url = quote(unquote(url, errors='replace'), safe=':/?&=%@[]')
resp = requests.get(url) Type guard
import re
def has_valid_percent_encoding(url: str) -> bool:
return not re.search(r'%[0-9A-Fa-f]{0,1}[^0-9A-Fa-f]', url) Try / catch
try:
resp = requests.get(url)
except requests.exceptions.InvalidURL:
from urllib.parse import quote
resp = requests.get(quote(url, safe=':/?&=%@[]')) Prevention
- Always properly encode user-supplied URL components with urllib.parse.quote.
- Validate URLs for malformed percent-encoding before passing to requests.
- Avoid constructing URLs by string concatenation with unencoded user input.
When it happens
Trigger: A URL containing a percent sign followed by non-ASCII alphanumeric characters (e.g., Unicode digits or letters from other scripts); extremely malformed percent-encoding in user-supplied URLs that gets past the isalnum() check due to Unicode properties.
Common situations: User-supplied URLs with Unicode characters after a percent sign; URLs copied from internationalized sources with encoding issues; extremely rare in practice because most malformed sequences fall into the else branch (just re-quoted).
Related errors
- {e}
- URL has an invalid label.
- cannot encode objects that are not 2-tuples
- {e.args}
- Invalid URL : No host supplied
AI-assisted analysis of psf/requests@8068356288 (2026-08-11).
Data as JSON: /api/errors/143063c1189eaac1.
Report an issue: GitHub.
Appendix: source
Thrown at src/requests/utils.py:693
UNRESERVED_SET: Final = frozenset(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~"
)
def unquote_unreserved(uri: str) -> str:
"""Un-escape any percent-escape sequences in a URI that are unreserved
characters. This leaves all reserved, illegal and non-ASCII bytes encoded.
:rtype: str
"""
parts = uri.split("%")
for i in range(1, len(parts)):
h = parts[i][0:2]
if len(h) == 2 and h.isalnum():
try:
c = chr(int(h, 16))
except ValueError:
raise InvalidURL(f"Invalid percent-escape sequence: '{h}'")
if c in UNRESERVED_SET:
parts[i] = c + parts[i][2:]
else:
parts[i] = f"%{parts[i]}"
else:
parts[i] = f"%{parts[i]}"
return "".join(parts)
def requote_uri(uri: str) -> str:
"""Re-quote the given URI.
This function passes the given URI through an unquote/quote cycle to
ensure that it is fully and consistently quoted.
:rtype: str
"""View on GitHub (pinned to 8068356288)