From 6929ab674df89d2f62a02767fedbe81f4654b570 Mon Sep 17 00:00:00 2001 From: Ekalabya Ghosh Date: Fri, 25 Sep 2026 23:16:01 +0530 Subject: [PATCH] gh-158182: fix AssertionError in http.client.putrequest on percent-encoded netloc _strip_ipv6_iface() is meant to strip an IPv6 zone id from a bracketed literal like [fe80::1%eth0], but putrequest() passes it the entire netloc of an absolute-URL request (the normal shape for HTTP-proxy requests). Any RFC 3986-legal percent-encoding earlier in the netloc than a zone id -- most plausibly percent-encoded userinfo, required whenever a username or password contains '@', ':', '/', or '%' -- made the function assert that the part before the first '%' starts with '[', raising a raw AssertionError instead of sending the request. Only treat the input as a bracketed IPv6 literal (and strip the zone id) when it actually starts with '['; otherwise return it unchanged. This also fixes the case the assertion was meant to guard when both userinfo and a zone id are present, since the previous code partitioned on the userinfo's '%' first. --- Lib/http/client.py | 15 ++++++++++----- ...2026-09-25-17-43-30.gh-issue-158182.2MgwWD.rst | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-25-17-43-30.gh-issue-158182.2MgwWD.rst diff --git a/Lib/http/client.py b/Lib/http/client.py index 7ef99e7201c005c..7dbdc8e496c13f7 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -186,11 +186,16 @@ def _encode(data, name='data'): def _strip_ipv6_iface(enc_name: bytes) -> bytes: """Remove interface scope from IPv6 address.""" - enc_name, percent, _ = enc_name.partition(b"%") - if percent: - assert enc_name.startswith(b'['), enc_name - enc_name += b']' - return enc_name + before, percent, after = enc_name.partition(b"%") + if not percent or not before.startswith(b'['): + # No '%' in the input, or it's not a bracketed IPv6 literal, so + # this isn't an IPv6 zone id -- e.g. RFC 3986 percent-encoding + # elsewhere in the netloc, most plausibly in userinfo (required + # whenever a username/password contains '@', ':', '/', or '%'). + # Leave it untouched rather than assuming the '%' we happened to + # find is a zone separator. + return enc_name + return before + b']' class HTTPMessage(email.message.Message): diff --git a/Misc/NEWS.d/next/Library/2026-09-25-17-43-30.gh-issue-158182.2MgwWD.rst b/Misc/NEWS.d/next/Library/2026-09-25-17-43-30.gh-issue-158182.2MgwWD.rst new file mode 100644 index 000000000000000..61188bed774fcf5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-25-17-43-30.gh-issue-158182.2MgwWD.rst @@ -0,0 +1,3 @@ +Fix :exc:`AssertionError` in :meth:`http.client.HTTPConnection.putrequest` +when an absolute-URL request's netloc contains RFC 3986 percent-encoding +outside of an IPv6 zone identifier, such as percent-encoded userinfo.