From a5f4f47021d15592deb3d78142c0a241228f6433 Mon Sep 17 00:00:00 2001 From: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:36:13 +0800 Subject: [PATCH] Percent-decode userinfo in URL credentials (httpie/httpie#1623) urlsplit() leaves the userinfo percent-encoded, so a URL like https://u%40d:1%3d2%3f@example.org was authenticated with the literal username 'u%40d' instead of 'u@d' (and a wrong password), causing spurious 401s. Reserved characters simply could not be supplied in the URL. URL-decode username and password extracted from the netloc (unquote) so behavior matches curl. Plain (non-encoded) credentials are unaffected; added a regression test. AI assistance disclosed: drafted with an LLM coding agent, verified locally (RED on main, GREEN with fix; full test_auth.py 33 passed). --- httpie/cli/argparser.py | 9 ++++++--- tests/test_auth.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index 9bf09b3b73..c37fefb85d 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -5,7 +5,7 @@ import sys from argparse import RawDescriptionHelpFormatter from textwrap import dedent -from urllib.parse import urlsplit +from urllib.parse import unquote, urlsplit from requests.utils import get_netrc_auth @@ -289,8 +289,11 @@ def _process_auth(self): if self.args.auth is None and not auth_type_set: if url.username is not None: # Handle http://username:password@hostname/ - username = url.username - password = url.password or '' + # Percent-decode the userinfo so reserved characters can be + # supplied directly in the URL (e.g. u%40d -> u@d); this matches + # curl's behavior. See https://github.com/httpie/httpie/issues/1623 + username = unquote(url.username) + password = unquote(url.password or '') self.args.auth = AuthCredentials( key=username, value=password, diff --git a/tests/test_auth.py b/tests/test_auth.py index 83423efec0..ca474d9ec7 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -80,6 +80,26 @@ def test_only_username_in_url(url): assert args.auth.password == '' +@pytest.mark.parametrize( + 'url, expected_username, expected_password', + [ + # Percent-encoded userinfo must be decoded (https://github.com/httpie/httpie/issues/1623) + ('https://u%40d:1%3d2%3f@example.org/', 'u@d', '1=2?'), + ('https://user%40example.com:pa%2Fss@example.org/', 'user@example.com', 'pa/ss'), + # Plain (non-encoded) credentials must be unaffected + ('https://alice:secret@example.org/', 'alice', 'secret'), + ('https://alice@example.org/', 'alice', ''), + ], +) +def test_credentials_in_url_are_percent_decoded(url, expected_username, expected_password): + """Userinfo in the URL should be percent-decoded so reserved characters + can be supplied directly (curl-compatible). See httpie/httpie#1623.""" + args = httpie.cli.definition.parser.parse_args(args=[url], env=MockEnvironment()) + assert args.auth + assert args.auth.username == expected_username + assert args.auth.password == expected_password + + def test_missing_auth(httpbin): r = http( '--auth-type=basic',