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. |
| |
| ifself.protocol.handshake_excisnotNone: |
| raiseself.protocol.handshake_exc |
conn_handler then hits this assertion, since the state is CONNECTING or CLOSED after a rejection, never OPEN:
| assertconnection.protocol.stateisOPEN |
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:
| exceptException: # 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:
importhttpimportthreadingfromwebsockets.sync.clientimportconnectfromwebsockets.sync.serverimportservedefreject(connection, request):
returnconnection.respond(http.HTTPStatus.FORBIDDEN, "Forbidden")
defhandler(connection):
raiseAssertionError("handler ran on a rejected connection")
withserve(handler, "localhost", 0, process_request=reject) asserver:
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port=server.socket.getsockname()
try:
connect(f"ws://{host}:{port}/")
exceptExceptionasexc:
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.