Integration7 min read

aiohttp proxies in 3.14: auth without BasicAuth, env vars and the errors you'll hit

Set up an HTTP or SOCKS proxy in aiohttp 3.14: replace the deprecated BasicAuth and proxy_auth, turn on trust_env, and read the 407 and Bad status line errors.

On this page

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():

python
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 ashttps:// URLhttp:// URLDeprecation warnings
proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(...)}200407none
proxy="http://user:pass@host:port"200200none
proxy_auth=aiohttp.BasicAuth(...)200200two 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:

python
async with session.get(url, proxy=PROXY, proxy_auth=aiohttp.BasicAuth("user", "pass")) as response:
    print(response.status)

Each call printed two warnings:

code
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)} instead

The replacement:

python
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 shows proxy_headers with an http:// URL. In the lab that request reached the proxy without Proxy-Authorization, and without a custom header we added to proxy_headers, and came back as 407. Issue #4422 reported the same against aiohttp 3.6.2. If you fetch http:// URLs, put the credentials in the proxy URL: proxy="http://user:pass@host:port" returned 200 for both schemes with no warning.
  • ClientSession has no proxy_headers argument. The documentation shows ClientSession(proxy=..., proxy_headers=...). In 3.14.4 it raised TypeError: ClientSession.__init__() got an unexpected keyword argument 'proxy_headers'. Set proxy= on the session and pass proxy_headers= with each request, as in the first example.
  • Don't move the header to headers=. For an https:// URL, request headers travel inside the tunnel. With Proxy-Authorization in headers=, the lab's destination server received the proxy credentials.
  • The default encoding changed. encode_basic_auth() uses UTF-8 and BasicAuth used latin1. For the password päss the two produced different header values, so pass encoding="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:

python
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_PROXY and lowercase http_proxy applied to http:// URLs, and HTTPS_PROXY to https:// URLs. With only HTTP_PROXY set, an https:// request went direct.
  • ALL_PROXY was ignored.
  • Credentials in the variable's URL authenticated both schemes and printed no warning.
  • NO_PROXY=127.0.0.1 sent the request direct. It had no effect on an explicit proxy= argument, which also took priority over the variable.
  • A variable holding an https:// proxy URL was skipped. aiohttp logged HTTPS proxies https://127.0.0.1:8888 are not supported, ignoring and 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:

code
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:

code
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:

code
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:

code
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

sh
python -m pip install aiohttp-socks
python
import 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. For http://localhost:8080/ the server logged a domain name. With rdns=False it logged 127.0.0.1.
  • ProxyConnector.from_url("socks5h://...") raised ValueError: 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.