In the threading implementation, when process_request or process_response rejects the handshake by returning an HTTP response, handshake() returns normally; handshake_exc is only set for failures, not for deliberate rejections:
|
# self.protocol.handshake_exc is set when the connection is lost before |
|
# receiving a request, when the request cannot be parsed, or when the |
|
# handshake fails, including when process_request or process_response |
|
# raises an exception. |
|
|
|
# It isn't set when process_request or process_response sends an HTTP |
|
# response that rejects the handshake. |
|
|
|
if self.protocol.handshake_exc is not None: |
|
raise self.protocol.handshake_exc |
conn_handler then hits this assertion, since the state is CONNECTING or CLOSED after a rejection, never OPEN:
|
assert connection.protocol.state is OPEN |
The AssertionError is swallowed by the except Exception below, so nothing is logged and the socket is closed without going through close_socket() and joining recv_events_thread. The # pragma: no cover there isn't accurate either; this branch runs on every rejection:
|
except Exception: # pragma: no cover |
|
# Don't leak sockets on unexpected errors. |
|
sock.close() |
With python -O, the assertion is stripped instead and the handler runs on the rejected connection: start_keepalive() and handler(connection) execute with the connection already CLOSED, and every rejection logs "connection handler failed".
Repro:
import http
import threading
from websockets.sync.client import connect
from websockets.sync.server import serve
def reject(connection, request):
return connection.respond(http.HTTPStatus.FORBIDDEN, "Forbidden")
def handler(connection):
raise AssertionError("handler ran on a rejected connection")
with serve(handler, "localhost", 0, process_request=reject) as server:
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.socket.getsockname()
try:
connect(f"ws://{host}:{port}/")
except Exception as exc:
print(f"client: {exc!r}")
server.shutdown()
Under python, the client gets the 403 and the server logs nothing; a trace hook shows the AssertionError firing at line 608 on every rejection. Under python -O, the server additionally logs "connection handler failed" with the handler's traceback.
Verified on main (ff4869b) with Python 3.13 on macOS.
In the threading implementation, when
process_requestorprocess_responserejects the handshake by returning an HTTP response,handshake()returns normally;handshake_excis only set for failures, not for deliberate rejections:websockets/src/websockets/sync/server.py
Lines 180 to 189 in ff4869b
conn_handlerthen hits this assertion, since the state is CONNECTING or CLOSED after a rejection, never OPEN:websockets/src/websockets/sync/server.py
Line 608 in ff4869b
The AssertionError is swallowed by the
except Exceptionbelow, so nothing is logged and the socket is closed without going throughclose_socket()and joiningrecv_events_thread. The# pragma: no coverthere isn't accurate either; this branch runs on every rejection:websockets/src/websockets/sync/server.py
Lines 618 to 620 in ff4869b
With
python -O, the assertion is stripped instead and the handler runs on the rejected connection:start_keepalive()andhandler(connection)execute with the connection already CLOSED, and every rejection logs "connection handler failed".Repro:
Under
python, the client gets the 403 and the server logs nothing; a trace hook shows the AssertionError firing at line 608 on every rejection. Underpython -O, the server additionally logs "connection handler failed" with the handler's traceback.Verified on main (ff4869b) with Python 3.13 on macOS.