Comparison7 min read

socks5 vs socks5h: what six clients actually send to the proxy

socks5:// does not mean local DNS in every client. We logged what curl, Requests, HTTPX, aiohttp, Playwright and Node send a SOCKS5 proxy for each scheme.

On this page

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 proxysocks5h://
curl 8.22.0an IPv4 address (resolved locally)the hostname
Requests 2.34.2 + PySocks 1.7.1an IPv4 address (resolved locally)the hostname
HTTPX 0.28.1 + socksio 1.0.0the hostnamethe hostname
aiohttp 3.14.3 + aiohttp-socks 0.12.0the hostnameValueError, no connection
Playwright 1.63.0 (Chromium 153.0.8010.12)the hostnamenet::ERR_NO_SUPPORTED_PROXIES, no connection
Node.js 24.20.0 + socks-proxy-agent 10.1.0an 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:

  1. 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
  2. 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.
  3. 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 a ConnectionError containing [Errno -2] Name or service not known, and socks-proxy-agent failed with getaddrinfo ENOTFOUND dnsprobe-missing.example. None of them sent the proxy a request. With socks5h://, 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

ClientSends the hostname to the proxyAvoid
curlsocks5h:// or --socks5-hostnamesocks5:// and --socks5 resolve locally
Requests + PySockssocks5h://socks5:// resolves locally
HTTPX + socksiosocks5:// or socks5h://socks:// raises ValueError
aiohttp + aiohttp-sockssocks5://socks5h:// raises ValueError
Playwright Chromiumsocks5://socks5h:// fails at navigation
Node socks-proxy-agentsocks5h:// 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:

code
ValueError: Invalid scheme component: socks5h

Playwright accepts the proxy in chromium.launch() and fails on the first navigation, without contacting the proxy:

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

code
127.0.0.2 dnsprobe.example

Start the server in one terminal and send one request per scheme from another:

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

code
LISTENING 127.0.0.1:1080
CMD=1 IPv4 127.0.0.2 port=80
CMD=1 DOMAIN dnsprobe.example port=80

The 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:port behaved like socks5h:// and --socks5 host:port like socks5:// in the test. curl man page
  • Requests: there is no separate option. urllib3's SOCKS manager sets PySocks' rdns flag from the scheme: False for socks5, True for socks5h. urllib3 source PySocks itself, used directly, takes rdns in set_proxy() and documents its default as True. PySocks README
  • aiohttp-socks: ProxyConnector takes rdns, 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) sent 127.0.0.2 and rdns=True sent the hostname.
  • HTTPX: version 0.28.1 routes socks5 and socks5h to the same SOCKS transport, so the scheme changes nothing. HTTPX source
  • socks-proxy-agent: the scheme alone sets its lookup flag: socks5 and socks4 resolve locally, socks5h, socks4a and socks do 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.

ipvolt is building proxy infrastructure for developers and agents. Join the waitlist for one email when access opens.

Sources

  1. curl man page: --proxy (socks5:// and socks5h://)
  2. RFC 1928: SOCKS Protocol Version 5, section 4 (address types)
  3. Requests: SOCKS proxies
  4. urllib3 2.8.0 source: contrib/socks.py (scheme to rdns)
  5. PySocks README: set_proxy and rdns
  6. HTTPX: SOCKS proxies
  7. HTTPX 0.28.1 source: proxy schemes in _transports/default.py
  8. aiohttp-socks 0.12.0 README: ProxyConnector and rdns
  9. python-socks 3.1.1 source: parse_proxy_url schemes
  10. socks-proxy-agent source: parseSocksURL
  11. Chromium network docs: SOCKSv5 proxy scheme and implicit bypass rules
  12. Playwright: browserType.launch proxy option
  13. RFC 7871: Client Subnet in DNS Queries, section 1

Tagged:ProxiesTroubleshooting