Skip to content

Client.close() hangs forever after a failed WebSocket connect #969

Description

@gedemagt

Observed behavior

WebSocketTransport.wait_closed() awaits self._close_task, but WebSocketTransport.close()
only assigns a real close task to _close_task when self._ws is set. If a wss connection
attempt fails before the WebSocket is established (TLS handshake reset, connection refused, DNS
failure, etc.), _ws is None, close() early-returns, and _close_task stays the unresolved
asyncio.Future() created in __init__. await self._close_task then never returns, so any
caller of Client.close() on such a client hangs indefinitely.

Offending code

nats/aio/transport.pyWebSocketTransport (2.14.0 line numbers):

def __init__(self, ...):
    self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
    self._client: aiohttp.ClientSession = aiohttp.ClientSession()
    self._close_task = asyncio.Future()        # L194: unresolved Future

async def wait_closed(self):
    await self._close_task                      # L248-249: awaits it
    if self._client:
        await self._client.close()
    self._ws = self._client = None

def close(self):
    if not self._ws:
        return                                  # L254-256: _ws is None → returns, _close_task stays unresolved
    self._close_task = asyncio.create_task(self._ws.close())

Client._close() (nats/aio/client.py) unconditionally calls both on a transport that exists:

if self._transport is not None:
    self._transport.close()
    try:
        await self._transport.wait_closed()     # hangs here forever
    except Exception as e:
        await self._error_cb(e)

self._transport is created in _connect_to_server/_select_next_server before the
ws_connect that fails, so it is non-None even though _ws was never set.

Expected behavior

Regardless of whether the WebSocketTransport fails or succeeds, the close()call should just close the connection.

Server and client version

Versions

  • Affected: 2.13.1 and 2.14.0 (latest at time of writing). Code is identical in both.
  • Python 3.13, allow_reconnect=True, transport wss:// (WebSocketTransport, aiohttp).

Host environment

No response

Steps to reproduce

Minimal example

import asyncio
from nats.aio.transport import WebSocketTransport

async def main():
    t = WebSocketTransport()      # _ws is None; _close_task is an unresolved Future
    t.close()                     # early-returns (no _ws); _close_task left unresolved
    await asyncio.wait_for(t.wait_closed(), timeout=2)  # raises TimeoutError — hangs

asyncio.run(main())

Output: asyncio.TimeoutErrorwait_closed() never returns.

Larger example

import asyncio
import nats

async def main():
    nc = nats.NATS()
    try:
        await nc.connect(
            "wss://127.0.0.1:1",          # nothing listening → ws_connect fails before _ws is set
            allow_reconnect=True,
            max_reconnect_attempts=1,
            reconnect_time_wait=0,
            connect_timeout=1,
        )
    except Exception as e:
        print("connect failed as expected:", type(e).__name__)

    await asyncio.wait_for(nc.close(), timeout=10)   # hangs → TimeoutError

asyncio.run(main())

Metadata

Metadata

Assignees

No one assigned

    Labels

    defectSuspected defect such as a bug or regression

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions