Two related diagnostics from production:
1) "Connection reset" instead of the custom block_message screen.
Root cause: writer.close() returned before the kernel flushed the
Login Disconnect packet, and the OS sent RST instead of FIN. Fix:
write_eof() + await wait_closed() so the FIN goes out after the
payload and the client has time to read the chat component.
2) Log entries showing reason "handshake error:" with an empty tail.
Root cause: bare OSError() / ConnectionResetError() have empty
str(), so the f-string interpolated to nothing. Fix: prepend the
exception class name so the reason is always informative.