aiohttp 3.14.0 (1 June 2026) deprecated BasicAuth and the proxy_auth parameter (changelog, #12499). The replacement is a Proxy-Authorization header built with aiohttp.encode_basic_auth():
import asyncio
import aiohttp
PROXY = "http://proxy.example.com:8080"
PROXY_HEADERS = {"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}
async def main() -> None:
async with aiohttp.ClientSession(proxy=PROXY) as session:
async with session.get("https://example.com/", proxy_headers=PROXY_HEADERS) as response:
print(response.status)
asyncio.run(main())In our lab this authenticated every https:// request with no DeprecationWarning. It did not authenticate plain http:// requests: aiohttp 3.14.4 sent proxy_headers only with the CONNECT request that opens an HTTPS tunnel, so for an http:// URL the proxy received no credentials and answered 407.
| Credentials passed as | https:// URL | http:// URL | Deprecation warnings |
|---|---|---|---|
proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(...)} | 200 | 407 | none |
proxy="http://user:pass@host:port" | 200 | 200 | none |
proxy_auth=aiohttp.BasicAuth(...) | 200 | 200 | two per call |
These results were recorded on 6 October 2026 with aiohttp 3.14.4 and aiohttp-socks 0.12.0 on Python 3.14.4, run with python -W always against a local proxy that requires Basic authentication. aiohttp 3.14.0 and 3.14.3 gave the same results.
Migrating from proxy_auth=aiohttp.BasicAuth
The old call still works in 3.14.4 for both URL schemes:
async with session.get(url, proxy=PROXY, proxy_auth=aiohttp.BasicAuth("user", "pass")) as response:
print(response.status)Each call printed two warnings:
DeprecationWarning: BasicAuth is deprecated and will be removed in aiohttp 4.0; use aiohttp.encode_basic_auth() with headers={'Authorization': ...} instead
DeprecationWarning: The 'proxy_auth' parameter is deprecated and will be removed in v4; pass proxy_headers={'Proxy-Authorization': aiohttp.encode_basic_auth(login, password)} insteadThe replacement:
proxy_headers = {"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}
async with session.get(url, proxy=PROXY, proxy_headers=proxy_headers) as response:
print(response.status)Check four things before you ship it:
- Plain
http://URLs lose their credentials. The documentation showsproxy_headerswith anhttp://URL. In the lab that request reached the proxy withoutProxy-Authorization, and without a custom header we added toproxy_headers, and came back as 407. Issue #4422 reported the same against aiohttp 3.6.2. If you fetchhttp://URLs, put the credentials in the proxy URL:proxy="http://user:pass@host:port"returned 200 for both schemes with no warning. ClientSessionhas noproxy_headersargument. The documentation showsClientSession(proxy=..., proxy_headers=...). In 3.14.4 it raisedTypeError: ClientSession.__init__() got an unexpected keyword argument 'proxy_headers'. Setproxy=on the session and passproxy_headers=with each request, as in the first example.- Don't move the header to
headers=. For anhttps://URL, request headers travel inside the tunnel. WithProxy-Authorizationinheaders=, the lab's destination server received the proxy credentials. - The default encoding changed.
encode_basic_auth()uses UTF-8 andBasicAuthused latin1. For the passwordpässthe two produced different header values, so passencoding="latin1"if your proxy expects the old bytes.
encode_basic_auth() is new in 3.14 (reference). On aiohttp 3.13.5 the same code raised AttributeError: module aiohttp has no attribute encode_basic_auth.
Environment variables need trust_env=True
A default ClientSession() ignores proxy variables. With HTTP_PROXY set, the request went straight to the target and the proxy logged nothing. This session used the proxy:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get("http://example.com/") as response:
print(response.status)The documentation says aiohttp reads the variables through urllib.request.getproxies(), applies them to the HTTP, HTTPS, WS and WSS schemes, lets hosts in no_proxy bypass the proxy, and can take proxy credentials from ~/.netrc. In the lab, with trust_env=True:
HTTP_PROXYand lowercasehttp_proxyapplied tohttp://URLs, andHTTPS_PROXYtohttps://URLs. With onlyHTTP_PROXYset, anhttps://request went direct.ALL_PROXYwas ignored.- Credentials in the variable's URL authenticated both schemes and printed no warning.
NO_PROXY=127.0.0.1sent the request direct. It had no effect on an explicitproxy=argument, which also took priority over the variable.- A variable holding an
https://proxy URL was skipped. aiohttp loggedHTTPS proxies https://127.0.0.1:8888 are not supported, ignoringand connected directly.
Proxy environment variables compares these rules across other clients.
ClientHttpProxyError: 407 on https:// URLs
When the proxy rejects the CONNECT request, aiohttp raises before anything reaches the destination. With a wrong password in the proxy URL:
aiohttp.client_exceptions.ClientHttpProxyError: 407, message='Proxy Authentication Required', url='http://user:wrong@127.0.0.1:8888'The password is in the message, so it goes wherever the exception goes: logs, tracebacks, error trackers. With the credentials in proxy_headers, or taken from HTTPS_PROXY, the same failure printed url='http://127.0.0.1:8888'.
Keep credentials out of the proxy URL for https:// traffic. Where you need them in the URL, log exc.status and exc.message (407 and Proxy Authentication Required in the lab) instead of the exception text. Fix proxy error 407 gives the order of checks when the credentials look right.
Status 407 with no exception on http:// URLs
For an http:// URL the proxy's 407 is an ordinary response. Nothing is raised, response.status is 407 and the Proxy-Authenticate header holds the challenge. Code that only catches exceptions treats this as success.
Check response.status, or create the session with raise_for_status=True. That raised:
aiohttp.client_exceptions.ClientResponseError: 407, message='Proxy Authentication Required', url='http://127.0.0.1:8080/'raise_for_status=True on the request and response.raise_for_status() raised the same error. The class is ClientResponseError and the URL is the target, not the proxy. ClientHttpProxyError is a subclass of it, so except aiohttp.ClientResponseError with a check for exc.status == 407 covers both schemes.
Bad status line: b'\x05\x00' from a socks5:// proxy URL
aiohttp does not reject proxy="socks5://...". It sends an HTTP request to the SOCKS port and fails on the reply:
aiohttp.client_exceptions.ClientResponseError: 400, message="Bad status line:\n Expected HTTP/, RTSP/ or ICE/:\n\n b'\\x05\\x00'\n ^", url='http://127.0.0.1:8080/'\x05\x00 is the start of the SOCKS5 server's reply, read as if it were an HTTP status line. socks5h:// failed the same way. The fix is a SOCKS connector, shown below.
ClientConnectorSSLError from an https:// proxy URL
An https:// proxy URL tells aiohttp to open TLS to the proxy itself. Against the lab proxy, which speaks plain HTTP, the request failed during the handshake:
aiohttp.client_exceptions.ClientConnectorSSLError: Cannot connect to host 127.0.0.1:8888 ssl:default [[SSL: RECORD_LAYER_FAILURE] record layer failure (_ssl.c:1081)]The proxy saw a TLS ClientHello and answered with a plaintext 400. The scheme of the proxy URL describes the connection to the proxy, not to the destination: an http:// proxy URL carries https:// requests through CONNECT. The error text comes from OpenSSL 3.5.5 and may read differently on other versions.
DeprecationWarning: BasicAuth is deprecated
Constructing aiohttp.BasicAuth(...) is enough to trigger the first warning, before any request. The auth= parameter for website credentials is deprecated too. Its warning names headers={'Authorization': aiohttp.encode_basic_auth(login, password)} as the replacement.
You may not see either warning. By default Python shows a DeprecationWarning only when the code that triggered it is in __main__. In the lab the deprecated call printed its warnings when it was in the script being run and printed nothing when it was in an imported module. python -W always printed them in both places. python -W error turned the first one into an exception, which finds every call site in a test run.
SOCKS proxies: use aiohttp-socks
python -m pip install aiohttp-socksimport aiohttp
from aiohttp_socks import ProxyConnector
connector = ProxyConnector.from_url("socks5://127.0.0.1:1080")
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get("https://example.com/") as response:
print(response.status)With aiohttp-socks 0.12.0 this returned 200 for http:// and https:// URLs, and the SOCKS server logged each connection. Two details:
socks5://already sends the hostname to the proxy. Forhttp://localhost:8080/the server logged a domain name. Withrdns=Falseit logged127.0.0.1.ProxyConnector.from_url("socks5h://...")raisedValueError: Invalid scheme component: socks5h. socks5 vs socks5h shows what each client does with the two schemes.
Requests and HTTPX need their own SOCKS packages, covered in Missing dependencies for SOCKS support.
Retries after proxy errors
A 407, the SOCKS status-line error and the TLS error above are configuration errors. The same request fails the same way again, so fix the setup instead of retrying. For timeouts and dropped connections, retry only requests that are safe to repeat. A POST may have reached the destination before the proxy connection failed. Proxy retries: one lost response, two created jobs shows that case and what an idempotency key changes.
If you use HTTPX for async work, see HTTPX async proxies. For diagnostic help inside a coding agent, Proxy Toolkit MCP accepts sanitized error descriptions. Keep credentials out of its arguments.
How this was tested
On Ubuntu 26.04.1 with Python 3.14.4 and OpenSSL 3.5.5, everything on 127.0.0.1: python -m http.server as the HTTP and HTTPS targets, a proxy that requires Basic authentication and logs every request with its header names, a SOCKS5 server that logs each destination, and an HTTPS target that reports the headers it received. Each of the 67 cases ran in a new interpreter with a clean environment. No proxy account was used.
The lab archive contains the runner, the cases, the proxy, the SOCKS5 server, the README and the recorded results.
Not tested: macOS and Windows, Digest and NTLM proxy authentication, authenticated SOCKS proxies, WebSocket requests, ~/.netrc credentials and proxies reached over TLS.
ipvolt is a proxy service for developers that isn't open yet; join the early-access list for one email when it opens.
Sources & further reading
Technical references used for this guide. Check the documentation for your installed version and your provider’s supported configuration.
- aiohttp changelog: 3.14.0
- aiohttp pull request #12499: Deprecate auth. Add encode_basic_auth
- aiohttp Advanced Client Usage: proxy support and trust_env
- aiohttp Client Reference: encode_basic_auth and proxy_headers
- aiohttp issue #4422: proxy_headers not passed to proxy
- aiohttp-socks on PyPI
- Python warnings: the default warning filter