Skip to content

gh-51540: Let proxy auth handlers answer a 407 to CONNECT for https URLs - #161596

Open
orsenthil wants to merge 1 commit into
python:mainfrom
orsenthil:issues/51540
Open

orsenthil wants to merge 1 commit into
python:mainfrom
orsenthil:issues/51540

Conversation

@orsenthil

@orsenthil orsenthil commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Python's Proxy Handler when it sent an authentication request simply failed as discussed in gh-51540.

ProxyBasicAuthHandler and ProxyDigestAuthHandler could not authenticate with a proxy for an https URL.

When the proxy answered the CONNECT request with 407 Proxy Authentication Required, http.client raised a plain OSError, do_open() wrapped it in URLError, and the http_error_407 handlers never ran. Only credentials embedded in the proxy URL worked (the 2009 fix on this issue).

With this change, a 407 sent by the origin through an established tunnel is still handled like one from the proxy; the proxy credentials are sent only on the CONNECT, never to the origin. This is documented on both http_error_407 methods and covered by a test.

Changes

  • http.client: add ProxyTunnelError, an OSError subclass raised when the proxy rejects CONNECT. It carries code, reason and headers; the message is unchanged, so except OSError keeps working. It holds no socket (the connection is closed before it is raised), which addresses the concern raised on the issue about exceptions that keep a socket open, and it can be pickled.
  • urllib.request.AbstractHTTPHandler.do_open(): a 407 to CONNECT is returned as an empty-bodied response, so the error handlers run and the request is retried with Proxy-Authorization on the CONNECT. Without credentials the caller now gets HTTPError 407 instead of URLError. Other CONNECT failures still raise URLError.
  • Request.set_proxy(): re-opening a tunnelled request (as any retry does) switched it to type='http' with the absolute URL as selector, so the retried request inside the tunnel was sent as GET https://host/path. It now keeps the tunnel.
  • AbstractDigestAuthHandler.get_authorization(): when tunnelling, the digest is computed over the CONNECT request (CONNECT host:port, RFC 7616 §3.4) rather than the request for the origin, so strict proxies accept it.

🤖 Generated with Claude Code

…ttps URLs

urllib.request could not authenticate with a proxy for an https URL
using ProxyBasicAuthHandler or ProxyDigestAuthHandler: http.client
raised a plain OSError when the proxy answered the CONNECT request
with 407, so the http_error_407 handlers never ran.

* http.client: add ProxyTunnelError, an OSError subclass carrying the
  code, reason and headers of the proxy's response.  It holds no
  socket and can be pickled.
* urllib.request: do_open() passes a 407 to CONNECT on to the error
  handlers, so the request is retried with Proxy-Authorization on the
  CONNECT.  Other CONNECT failures still raise URLError.
* Request.set_proxy() keeps the tunnel when a request is re-opened, so
  the retried request still sends the path, not the absolute URL.
* ProxyDigestAuthHandler computes the digest over the CONNECT request
  (CONNECT host:port) when tunnelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #35726799 | 📁 Comparing 57ab518 against main (1955eb3)

  🔍 Preview build  

4 files changed
± library/http.client.html
± library/urllib.request.html
± whatsnew/3.16.html
± whatsnew/changelog.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant