In curl, socks5:// resolves the destination hostname on your machine and socks5h:// hands the hostname to the proxy. Other clients do not follow that rule: in this test three of six sent the hostname with plain socks5://, and two of those refused socks5h:// before connecting.
| Client (version tested) | socks5:// sends the proxy | socks5h:// |
|---|---|---|
| curl 8.22.0 | an IPv4 address (resolved locally) | the hostname |
| Requests 2.34.2 + PySocks 1.7.1 | an IPv4 address (resolved locally) | the hostname |
| HTTPX 0.28.1 + socksio 1.0.0 | the hostname | the hostname |
| aiohttp 3.14.3 + aiohttp-socks 0.12.0 | the hostname | ValueError, no connection |
| Playwright 1.63.0 (Chromium 153.0.8010.12) | the hostname | net::ERR_NO_SUPPORTED_PROXIES, no connection |
| Node.js 24.20.0 + socks-proxy-agent 10.1.0 | an IPv4 address (resolved locally) | the hostname |
Each cell is what a logging SOCKS5 server on 127.0.0.1 received when the client was asked for http://dnsprobe.example/, a name that exists only in the local hosts file as 127.0.0.2. "An IPv4 address" means the proxy saw 127.0.0.2 and never learned the name. "The hostname" means it received dnsprobe.example and would have to resolve it. Every client gave the same result for an https:// target.
Why the address type matters
A SOCKS5 request carries one destination address, and its ATYP byte says which kind: X'01' for IPv4, X'03' for a domain name, X'04' for IPv6. RFC 1928, section 4 A client that sends a domain name leaves DNS to the proxy. A client that sends an IP address has already done the lookup itself. That has three consequences:
- Your own resolver sees the lookup. The DNS query for the destination goes to the resolver configured on your machine, outside the proxy connection. The Requests documentation says it directly:
socks5"causes the DNS resolution to happen on the client, rather than on the proxy server". Requests: SOCKS - The answer can be for the wrong place. RFC 7871 notes that many authoritative nameservers "return different responses based on the perceived topological location of the user", judged from the address the query arrives from, which is normally your resolver. RFC 7871, section 1 A CDN hostname resolved locally can therefore point at a server chosen for your network, and the proxy connects to exactly that address. The exit IP is in the region you asked for, but the server it reached was picked for yours, so the content can be the wrong region's. This follows from how the request is built; the loopback test did not measure any CDN.
- Names only the proxy can resolve fail. With a name that does not resolve locally, curl 8.22.0 stopped at
curl: (6) Could not resolve host: dnsprobe-missing.example, Requests raised aConnectionErrorcontaining[Errno -2] Name or service not known, and socks-proxy-agent failed withgetaddrinfo ENOTFOUND dnsprobe-missing.example. None of them sent the proxy a request. Withsocks5h://, all three sent the name to the proxy.
The HTTP vs SOCKS5 guide covers where DNS placement fits when you choose a proxy protocol. The rest of this post is about getting the scheme right per client.
What to put in your config
| Client | Sends the hostname to the proxy | Avoid |
|---|---|---|
| curl | socks5h:// or --socks5-hostname | socks5:// and --socks5 resolve locally |
| Requests + PySocks | socks5h:// | socks5:// resolves locally |
| HTTPX + socksio | socks5:// or socks5h:// | socks:// raises ValueError |
| aiohttp + aiohttp-socks | socks5:// | socks5h:// raises ValueError |
| Playwright Chromium | socks5:// | socks5h:// fails at navigation |
| Node socks-proxy-agent | socks5h:// or socks:// | socks5:// resolves locally |
No single URL is right for all six. If one PROXY_URL setting feeds several tools, socks5h:// breaks aiohttp-socks and Playwright, and socks5:// makes curl, Requests and socks-proxy-agent resolve locally. Set the scheme per client.
The two clients that reject socks5h:// fail at different moments. aiohttp-socks raises from ProxyConnector.from_url(), before any socket is opened:
ValueError: Invalid scheme component: socks5hPlaywright accepts the proxy in chromium.launch() and fails on the first navigation, without contacting the proxy:
page.goto: net::ERR_NO_SUPPORTED_PROXIES at http://dnsprobe.example/Playwright's documentation gives socks5://myproxy.com:3128 as the SOCKS form of the server option. Playwright: proxy option The Playwright proxy guide has the rest of the launch and context settings.
The bare socks:// scheme is just as inconsistent. Chromium and socks-proxy-agent treated it as SOCKS5 and sent the hostname. curl opened with a SOCKS4 greeting (first byte 0x04), which a SOCKS5-only server cannot answer. Requests raised ValueError: Unable to determine SOCKS version from socks://127.0.0.1:11080, HTTPX raised ValueError: Unknown scheme for proxy URL URL('socks://127.0.0.1:11080') and aiohttp-socks raised ValueError: Invalid scheme component: socks.
Two setup details from the Python side: Requests needs the requests[socks] extra and HTTPX needs httpx[socks]. Without them Requests raises Missing dependencies for SOCKS support and HTTPX raises an ImportError naming socksio; the fix for both also covers the case where the proxy URL comes from ALL_PROXY. The Python Requests proxy guide and the HTTPX async proxy guide show complete client code.
This test passed the proxy URL to each client explicitly. If yours comes from ALL_PROXY or HTTPS_PROXY, first confirm which variable the client reads; the proxy environment variables guide lists that per client, and NO_PROXY matching, tested shows that bypass rules differ between clients as well.
How to check your own setup
You do not need a proxy account to see what your client sends. Download socks5_log.py, a 42-line Python script with no dependencies. It completes the SOCKS5 greeting, prints the address type and address of each request, then answers "general SOCKS server failure", so nothing is forwarded.
Give your machine a test name that only the hosts file knows. Do not use localhost: Chromium sends requests for localhost names directly instead of through the proxy. Chromium: implicit bypass rules Add this line to /etc/hosts:
127.0.0.2 dnsprobe.exampleStart the server in one terminal and send one request per scheme from another:
python3 socks5_log.py 1080
curl -x socks5://127.0.0.1:1080 http://dnsprobe.example/
curl -x socks5h://127.0.0.1:1080 http://dnsprobe.example/With curl 8.18.0 the server printed:
LISTENING 127.0.0.1:1080
CMD=1 IPv4 127.0.0.2 port=80
CMD=1 DOMAIN dnsprobe.example port=80The first request arrived as an IP address, so curl resolved the name locally. The second arrived as a name. Both curl commands end with curl: (97) cannot complete SOCKS5 connection to dnsprobe.example. (1), which is the server's refusal and is expected. Replace curl with your own client and proxy setting, and read the same two fields. If no line appears at all, the client rejected the scheme or bypassed the proxy.
Remote DNS options besides the scheme
Some clients expose the same choice as an option, usually named rdns:
- curl:
--socks5-hostname host:portbehaved likesocks5h://and--socks5 host:portlikesocks5://in the test. curl man page - Requests: there is no separate option. urllib3's SOCKS manager sets PySocks'
rdnsflag from the scheme:Falseforsocks5,Trueforsocks5h. urllib3 source PySocks itself, used directly, takesrdnsinset_proxy()and documents its default asTrue. PySocks README - aiohttp-socks:
ProxyConnectortakesrdns, and its README notes "default is True for socks5". aiohttp-socks README In the test,ProxyConnector.from_url("socks5://127.0.0.1:11080", rdns=False)sent127.0.0.2andrdns=Truesent the hostname. - HTTPX: version 0.28.1 routes
socks5andsocks5hto the same SOCKS transport, so the scheme changes nothing. HTTPX source - socks-proxy-agent: the scheme alone sets its
lookupflag:socks5andsocks4resolve locally,socks5h,socks4aandsocksdo not. socks-proxy-agent source - Chromium: no option. Its documentation says that for SOCKSv5 "name resolution is always done proxy side". Chromium network docs
Method and limits
Recorded on 4 October 2026 on Ubuntu 26.04 (x86_64) with Python 3.14.4 and Node.js 24.20.0. The versions are the ones in the first table, plus urllib3 2.8.0, httpcore 1.0.9, python-socks 3.1.1 and the socks package 2.8.10. curl 8.18.0 was run next to 8.22.0 and sent the same addresses. A name that resolves only to IPv6 arrived as an IPv6 address from curl, Requests and socks-proxy-agent, and as a hostname from the other three.
The lab is one loopback server that refuses every request. It says nothing about any proxy service, about SOCKS5 with authentication or UDP, or about macOS, Windows, Firefox or WebKit. Library defaults change, so check the versions you run. The lab archive contains the server, the six client scripts, the runner and a README; the raw output of all 62 cases is in results.json.
Related
- HTTP vs SOCKS5 proxies, for choosing the protocol and deciding where DNS runs.
- Proxy environment variables, for which variable each client reads.
- NO_PROXY matching, tested, the same kind of test for bypass rules.
- Playwright proxy setup, HTTPX async proxy and Python Requests proxy, for full client configuration.
ipvolt is building proxy infrastructure for developers and agents. Join the waitlist for one email when access opens.
Sources
- curl man page: --proxy (socks5:// and socks5h://)
- RFC 1928: SOCKS Protocol Version 5, section 4 (address types)
- Requests: SOCKS proxies
- urllib3 2.8.0 source: contrib/socks.py (scheme to rdns)
- PySocks README: set_proxy and rdns
- HTTPX: SOCKS proxies
- HTTPX 0.28.1 source: proxy schemes in _transports/default.py
- aiohttp-socks 0.12.0 README: ProxyConnector and rdns
- python-socks 3.1.1 source: parse_proxy_url schemes
- socks-proxy-agent source: parseSocksURL
- Chromium network docs: SOCKSv5 proxy scheme and implicit bypass rules
- Playwright: browserType.launch proxy option
- RFC 7871: Client Subnet in DNS Queries, section 1