Skip to content

_init_socks5_connection ignores timeout — SyncStream.read() puts socket into infinite blocking mode #1089

Description

@easy6699

When using a SOCKS5 proxy with a sync httpx.Client, requests can hang indefinitely despite a timeout being configured. The hang occurs during the SOCKS5 handshake, before any HTTP traffic is sent.

Root cause

_init_socks5_connection in socks_proxy.py calls stream.read() three times — for the auth method response, the username/password response, and the connect response — without passing a timeout argument:

# socks_proxy.py, lines 62, 81, 97
incoming_bytes = stream.read(max_bytes=4096)

SyncStream.read() in sync.py accepts timeout: float | None = None and calls self._sock.settimeout(timeout) before every recv():

# sync.py, line 127
def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
    exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
    with map_exceptions(exc_map):
        self._sock.settimeout(timeout)  # timeout is None here
        return self._sock.recv(max_bytes)

Calling settimeout(None) puts the socket into infinite blocking mode, overriding:

  • any timeout the socket was created with via socket.create_connection(..., timeout=...)
  • any process-wide default set via socket.setdefaulttimeout()

As a result, if a SOCKS5 proxy accepts the TCP connection but then stalls — a common occurrence with Tor circuit establishment — recv() blocks forever and the entire process hangs. The timeout passed to httpx.Client(timeout=...) has no effect because it is never forwarded to these stream.read() calls.

The async backend does not have this bug — _init_socks5_connection in socks_proxy.py passes timeout through correctly.

To reproduce

import httpx

# Connect to a SOCKS5 proxy that accepts the TCP connection but stalls
# on the handshake response (simulate with: nc -l 9150)
proxy = httpx.Proxy("socks5://127.0.0.1:9150")
with httpx.Client(proxy=proxy, timeout=5.0) as client:
    client.get("http://example.com")  # hangs forever, timeout has no effect

Expected behavior

The timeout value passed to httpx.Client (and forwarded via request.extensions["timeout"]["connect"]) should apply to all I/O during the SOCKS5 handshake, including the stream.read() calls in _init_socks5_connection. A ConnectTimeout should be raised after the configured timeout elapses.

Actual behavior

recv() blocks indefinitely. No exception is raised. Ctrl+C shows the process stuck at:

File "httpcore/_sync/socks_proxy.py", line 97, in _init_socks5_connection
    incoming_bytes = stream.read(max_bytes=4096)
File "httpcore/_backends/sync.py", line 128, in read
    return self._sock.recv(max_bytes)
KeyboardInterrupt

Suggested fix

_init_socks5_connection receives the stream but not the timeout. The timeout is available in Socks5Connection.handle_request as timeouts.get("connect", None). It should be passed through to _init_socks5_connection and forwarded to each stream.read() call:

# socks_proxy.py
def _init_socks5_connection(
    stream: NetworkStream,
    *,
    host: bytes,
    port: int,
    auth: tuple[bytes, bytes] | None = None,
    timeout: float | None = None,   # <-- add this
) -> None:
    ...
    incoming_bytes = stream.read(max_bytes=4096, timeout=timeout)  # all 3 call sites

And in handle_request:

kwargs = {
    "stream": stream,
    "host": ...,
    "port": ...,
    "auth": self._proxy_auth,
    "timeout": timeout,   # <-- forward it
}

Note: the async counterpart (socks_proxy.py) already passes timeout correctly and does not need fixing.

My Test Environment

httpcore: 1.0.9
httpx: 0.28.1
Python: 3.14.4
OS: Debian-based Linux
Proxy type: SOCKS5

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions