Describe the bug
Client-side connect handler failures and return values are ignored by the Engine.IO client. When the connect handler raises or returns False (intending to refuse the connection), _trigger_event() catches the exception / returns the value, but the call sites in _connect_polling() and _connect_websocket() (both client.py and async_client.py) ignore the result. The client state is set to 'connected' before the handler runs and is never reverted, so the client ends up in a pseudo-connected state even though the connection was refused, and connect() returns without raising.
This matters in real usage: the Socket.IO layer registers a connect handler (_handle_eio_connect) that evaluates the Socket.IO auth value, which may be a callable (dynamic auth provider). If that provider raises, the exception is swallowed here, the Socket.IO CONNECT packet is never sent, and the client believes it is connected.
To Reproduce
import threading
from wsgiref.simple_server import make_server
import engineio
# local server
srv = engineio.Server()
httpd = make_server('127.0.0.1', 0, engineio.WSGIApp(srv))
threading.Thread(target=httpd.serve_forever, daemon=True).start()
port = httpd.server_address[1]
# client whose connect handler raises (or returns False to refuse)
eio = engineio.Client()
@eio.on('connect')
def on_connect():
raise RuntimeError('auth provider failed')
# or: return False # intent: refuse the connection
eio.connect(f'http://127.0.0.1:{port}', transports=['polling'])
print(eio.state) # prints 'connected' -- the exception/refusal was ignored
The same behavior reproduces with engineio.AsyncClient (_connect_polling / _connect_websocket in async_client.py).
Expected behavior
When the connect handler raises or returns False, the connection should be aborted and connect() should raise engineio.exceptions.ConnectionError, mirroring the server-side semantics where the handler's return value controls whether the client is accepted (server side: returning False refuses the connection; raising is also treated as a refusal). The client state should not remain 'connected'.
Logs
With logging enabled, the swallowed exception is only reported via:
engineio.client ERROR: connect handler error
Traceback (most recent call last):
...
RuntimeError: auth provider failed
and the connection proceeds as if nothing happened.
Additional context
Tested on python-engineio 4.12.3 and 4.13.5 (latest) — identical behavior.
Use case: we maintain a Python SDK (A2C-SMCP, an Agent-to-Computer remote tool-calling protocol) whose agent client passes auth to socketio.Client / AsyncClient as a callable — a pluggable credential provider that may fetch a short-lived token from a vault/API at connect time. When that provider fails, we need connect() to fail promptly with a meaningful ConnectionError; today the client instead enters a pseudo-connected state and the caller only later sees a confusing Socket.IO-level error with an empty namespace list (see related issues miguelgrinberg/python-socketio#1595 and miguelgrinberg/python-socketio#1594 — "One or more namespaces failed to connect: ").
A related consequence: the bare except: in _trigger_event() also swallows CancelledError (in Python 3.8+ it derives from BaseException, not Exception), so even cancelling a hung connect from outside (e.g. asyncio.wait_for / task cancellation) is silently ignored and the connection continues as if accepted.
Describe the bug
Client-side
connecthandler failures and return values are ignored by the Engine.IO client. When theconnecthandler raises or returnsFalse(intending to refuse the connection),_trigger_event()catches the exception / returns the value, but the call sites in_connect_polling()and_connect_websocket()(bothclient.pyandasync_client.py) ignore the result. The client state is set to'connected'before the handler runs and is never reverted, so the client ends up in a pseudo-connected state even though the connection was refused, andconnect()returns without raising.This matters in real usage: the Socket.IO layer registers a
connecthandler (_handle_eio_connect) that evaluates the Socket.IOauthvalue, which may be a callable (dynamic auth provider). If that provider raises, the exception is swallowed here, the Socket.IO CONNECT packet is never sent, and the client believes it is connected.To Reproduce
The same behavior reproduces with
engineio.AsyncClient(_connect_polling/_connect_websocketinasync_client.py).Expected behavior
When the
connecthandler raises or returnsFalse, the connection should be aborted andconnect()should raiseengineio.exceptions.ConnectionError, mirroring the server-side semantics where the handler's return value controls whether the client is accepted (server side: returningFalserefuses the connection; raising is also treated as a refusal). The client state should not remain'connected'.Logs
With logging enabled, the swallowed exception is only reported via:
and the connection proceeds as if nothing happened.
Additional context
Tested on python-engineio 4.12.3 and 4.13.5 (latest) — identical behavior.
Use case: we maintain a Python SDK (A2C-SMCP, an Agent-to-Computer remote tool-calling protocol) whose agent client passes
authtosocketio.Client/AsyncClientas a callable — a pluggable credential provider that may fetch a short-lived token from a vault/API at connect time. When that provider fails, we needconnect()to fail promptly with a meaningfulConnectionError; today the client instead enters a pseudo-connected state and the caller only later sees a confusing Socket.IO-level error with an empty namespace list (see related issues miguelgrinberg/python-socketio#1595 and miguelgrinberg/python-socketio#1594 — "One or more namespaces failed to connect: ").A related consequence: the bare
except:in_trigger_event()also swallowsCancelledError(in Python 3.8+ it derives fromBaseException, notException), so even cancelling a hung connect from outside (e.g.asyncio.wait_for/ task cancellation) is silently ignored and the connection continues as if accepted.