{"record":{"id":"143063c1189eaac1","repo":"psf/requests","slug":"invalid-percent-escape-sequence-h","errorCode":null,"errorMessage":"Invalid percent-escape sequence: '{h}'","messagePattern":"Invalid percent-escape sequence: '(.+?)'","errorType":"exception","errorClass":"InvalidURL","httpStatus":null,"severity":"error","filePath":"src/requests/utils.py","lineNumber":693,"sourceCode":"UNRESERVED_SET: Final = frozenset(\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\" + \"0123456789-._~\"\n)\n\n\ndef unquote_unreserved(uri: str) -> str:\n    \"\"\"Un-escape any percent-escape sequences in a URI that are unreserved\n    characters. This leaves all reserved, illegal and non-ASCII bytes encoded.\n\n    :rtype: str\n    \"\"\"\n    parts = uri.split(\"%\")\n    for i in range(1, len(parts)):\n        h = parts[i][0:2]\n        if len(h) == 2 and h.isalnum():\n            try:\n                c = chr(int(h, 16))\n            except ValueError:\n                raise InvalidURL(f\"Invalid percent-escape sequence: '{h}'\")\n\n            if c in UNRESERVED_SET:\n                parts[i] = c + parts[i][2:]\n            else:\n                parts[i] = f\"%{parts[i]}\"\n        else:\n            parts[i] = f\"%{parts[i]}\"\n    return \"\".join(parts)\n\n\ndef requote_uri(uri: str) -> str:\n    \"\"\"Re-quote the given URI.\n\n    This function passes the given URI through an unquote/quote cycle to\n    ensure that it is fully and consistently quoted.\n\n    :rtype: str\n    \"\"\"","sourceCodeStart":675,"sourceCodeEnd":711,"githubUrl":"https://github.com/psf/requests/blob/8068356288978c4f54661ae6f95afe0e0831885e/src/requests/utils.py#L675-L711","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"# before\nrequests.get('https://example.com/path%ZZbad')  # potential InvalidURL\n\n# after — sanitize the URL first\nfrom urllib.parse import quote, unquote\nsafe_url = quote(unquote(url), safe=':/?&=%')\nrequests.get(safe_url)","handlingStrategy":"validation","validationCode":"import re\nif re.search(r'%[^0-9A-Fa-f]', url):\n    from urllib.parse import quote, unquote\n    url = quote(unquote(url, errors='replace'), safe=':/?&=%@[]')\nresp = requests.get(url)","typeGuard":"import re\ndef has_valid_percent_encoding(url: str) -> bool:\n    return not re.search(r'%[0-9A-Fa-f]{0,1}[^0-9A-Fa-f]', url)","tryCatchPattern":"try:\n    resp = requests.get(url)\nexcept requests.exceptions.InvalidURL:\n    from urllib.parse import quote\n    resp = requests.get(quote(url, safe=':/?&=%@[]'))","preventionTips":["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."],"tags":["invalid-url","percent-encoding","url","unicode","utils"],"backgroundTag":null,"analyzedSha":"8068356288978c4f54661ae6f95afe0e0831885e","analyzedAt":"2026-08-11T20:11:09.238Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}