From f5a995de0bcfb95e3e1f084b032ac3e1d9673c1d Mon Sep 17 00:00:00 2001 From: "Michiel W. Beijen" Date: Tue, 2 Jun 2026 08:06:03 +0200 Subject: [PATCH] Propagate timeout through SOCKS5 handshake During SOCKS5 connection setup, all reads and writes to the proxy socket were called without a timeout. A non-responsive proxy would block the calling thread/task indefinitely regardless of any `timeout` configured on the request. Thread the `connect` timeout (already used for the initial TCP connect) through `_init_socks5_connection()` to every `stream.read()` and `stream.write()` call. Ported from encode/httpcore#1055 (drshvik), via codeberg.org/httpxyz/httpcorexyz@0387930. Co-Authored-By: drshvik --- src/httpcore2/httpcore2/_async/socks_proxy.py | 14 ++++++++------ src/httpcore2/httpcore2/_sync/socks_proxy.py | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/httpcore2/httpcore2/_async/socks_proxy.py b/src/httpcore2/httpcore2/_async/socks_proxy.py index 0c44ab33..44149062 100644 --- a/src/httpcore2/httpcore2/_async/socks_proxy.py +++ b/src/httpcore2/httpcore2/_async/socks_proxy.py @@ -45,6 +45,7 @@ async def _init_socks5_connection( host: bytes, port: int, auth: tuple[bytes, bytes] | None = None, + timeout: float | None = None, ) -> None: conn = socksio.socks5.SOCKS5Connection() @@ -56,10 +57,10 @@ async def _init_socks5_connection( ) conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method])) outgoing_bytes = conn.data_to_send() - await stream.write(outgoing_bytes) + await stream.write(outgoing_bytes, timeout=timeout) # Auth method response - incoming_bytes = await stream.read(max_bytes=4096) + incoming_bytes = await stream.read(max_bytes=4096, timeout=timeout) response = conn.receive_data(incoming_bytes) assert isinstance(response, socksio.socks5.SOCKS5AuthReply) if response.method != auth_method: @@ -73,10 +74,10 @@ async def _init_socks5_connection( username, password = auth conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password)) outgoing_bytes = conn.data_to_send() - await stream.write(outgoing_bytes) + await stream.write(outgoing_bytes, timeout=timeout) # Username/password response - incoming_bytes = await stream.read(max_bytes=4096) + incoming_bytes = await stream.read(max_bytes=4096, timeout=timeout) response = conn.receive_data(incoming_bytes) assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply) if not response.success: @@ -85,10 +86,10 @@ async def _init_socks5_connection( # Connect request conn.send(socksio.socks5.SOCKS5CommandRequest.from_address(socksio.socks5.SOCKS5Command.CONNECT, (host, port))) outgoing_bytes = conn.data_to_send() - await stream.write(outgoing_bytes) + await stream.write(outgoing_bytes, timeout=timeout) # Connect response - incoming_bytes = await stream.read(max_bytes=4096) + incoming_bytes = await stream.read(max_bytes=4096, timeout=timeout) response = conn.receive_data(incoming_bytes) assert isinstance(response, socksio.socks5.SOCKS5Reply) if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED: @@ -229,6 +230,7 @@ async def handle_async_request(self, request: Request) -> Response: "host": self._remote_origin.host.decode("ascii"), "port": self._remote_origin.port, "auth": self._proxy_auth, + "timeout": timeout, } async with Trace("setup_socks5_connection", logger, request, kwargs) as trace: await _init_socks5_connection(**kwargs) diff --git a/src/httpcore2/httpcore2/_sync/socks_proxy.py b/src/httpcore2/httpcore2/_sync/socks_proxy.py index c7744411..54329201 100644 --- a/src/httpcore2/httpcore2/_sync/socks_proxy.py +++ b/src/httpcore2/httpcore2/_sync/socks_proxy.py @@ -45,6 +45,7 @@ def _init_socks5_connection( host: bytes, port: int, auth: tuple[bytes, bytes] | None = None, + timeout: float | None = None, ) -> None: conn = socksio.socks5.SOCKS5Connection() @@ -56,10 +57,10 @@ def _init_socks5_connection( ) conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method])) outgoing_bytes = conn.data_to_send() - stream.write(outgoing_bytes) + stream.write(outgoing_bytes, timeout=timeout) # Auth method response - incoming_bytes = stream.read(max_bytes=4096) + incoming_bytes = stream.read(max_bytes=4096, timeout=timeout) response = conn.receive_data(incoming_bytes) assert isinstance(response, socksio.socks5.SOCKS5AuthReply) if response.method != auth_method: @@ -73,10 +74,10 @@ def _init_socks5_connection( username, password = auth conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password)) outgoing_bytes = conn.data_to_send() - stream.write(outgoing_bytes) + stream.write(outgoing_bytes, timeout=timeout) # Username/password response - incoming_bytes = stream.read(max_bytes=4096) + incoming_bytes = stream.read(max_bytes=4096, timeout=timeout) response = conn.receive_data(incoming_bytes) assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply) if not response.success: @@ -85,10 +86,10 @@ def _init_socks5_connection( # Connect request conn.send(socksio.socks5.SOCKS5CommandRequest.from_address(socksio.socks5.SOCKS5Command.CONNECT, (host, port))) outgoing_bytes = conn.data_to_send() - stream.write(outgoing_bytes) + stream.write(outgoing_bytes, timeout=timeout) # Connect response - incoming_bytes = stream.read(max_bytes=4096) + incoming_bytes = stream.read(max_bytes=4096, timeout=timeout) response = conn.receive_data(incoming_bytes) assert isinstance(response, socksio.socks5.SOCKS5Reply) if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED: @@ -229,6 +230,7 @@ def handle_request(self, request: Request) -> Response: "host": self._remote_origin.host.decode("ascii"), "port": self._remote_origin.port, "auth": self._proxy_auth, + "timeout": timeout, } with Trace("setup_socks5_connection", logger, request, kwargs) as trace: _init_socks5_connection(**kwargs)