Skip to content

Client-side connect handler failures and return values are ignored: client enters pseudo-connected state #462

Description

@JIAQIA

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions