WebSocketDisconnect is your tell
When the client closes — clean or abrupt — the next receive_* call raises WebSocketDisconnect. The exception carries .code (the close code) and .reason (the close frame's reason). This is your single authoritative signal that the session is ending.
Cleanup belongs in the except block
Whatever you set up on connect (added to a connection manager, registered a heartbeat task, opened a database row) must be torn down here. The pattern is connect → register → try/loop/except → unregister. Do not skip the unregister; orphaned references are how WebSocket servers leak memory at scale.
Other exceptions still happen
Application bugs in your message handler will throw their own exceptions. Catch them separately so you can return a structured error to the client and still close cleanly.