From 5f9fa15a6ad4fac206b814fdf2537eee78a818db Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Thu, 19 Mar 2026 18:02:07 +0100 Subject: [PATCH] Realtime - Suppress socket.io errors in logs (#3991) --- changedetectionio/realtime/socket_server.py | 23 +++++++++++++++++++++ changedetectionio/static/js/realtime.js | 8 +++++++ 2 files changed, 31 insertions(+) diff --git a/changedetectionio/realtime/socket_server.py b/changedetectionio/realtime/socket_server.py index c9b48c5e9..3c20c285a 100644 --- a/changedetectionio/realtime/socket_server.py +++ b/changedetectionio/realtime/socket_server.py @@ -199,8 +199,31 @@ def handle_watch_update(socketio, **kwargs): logger.error(f"Socket.IO error in handle_watch_update: {str(e)}") +def _suppress_werkzeug_ws_abrupt_disconnect_noise(): + """Patch BaseWSGIServer.log to suppress the AssertionError traceback that fires when + a browser closes a WebSocket connection mid-handshake (e.g. closing a tab). + The exception is caught inside run_wsgi and routed to self.server.log() — it never + propagates out, so wrapping run_wsgi doesn't help. Patching the log method is the + only reliable intercept point. The error is cosmetic: Socket.IO already handles the + disconnect correctly via its own disconnect handler and timeout logic.""" + try: + from werkzeug.serving import BaseWSGIServer + _original_log = BaseWSGIServer.log + + def _filtered_log(self, type, message, *args): + if type == 'error' and 'write() before start_response' in message: + return + _original_log(self, type, message, *args) + + BaseWSGIServer.log = _filtered_log + except Exception: + pass + + def init_socketio(app, datastore): """Initialize SocketIO with the main Flask app""" + _suppress_werkzeug_ws_abrupt_disconnect_noise() + import platform import sys diff --git a/changedetectionio/static/js/realtime.js b/changedetectionio/static/js/realtime.js index 474b2df0b..9d52546ce 100644 --- a/changedetectionio/static/js/realtime.js +++ b/changedetectionio/static/js/realtime.js @@ -116,6 +116,14 @@ $(document).ready(function () { $('#realtime-conn-error').show(); }); + // Tell the server we're leaving cleanly so it can release the connection + // immediately rather than waiting for a timeout. + // Note: this only fires for voluntary closes (tab/window close, navigation away). + // Hard kills, crashes and network drops will still timeout normally on the server. + window.addEventListener('beforeunload', function () { + socket.disconnect(); + }); + socket.on('queue_size', function (data) { console.log(`${data.event_timestamp} - Queue size update: ${data.q_length}`); if(queueSizePagerInfoText) {