From 455bc05c6909f356759119b942e3450ad9dda37b Mon Sep 17 00:00:00 2001 From: Smittix Date: Sun, 8 Feb 2026 13:40:17 +0000 Subject: [PATCH] Shut down WebSocket socket to prevent Werkzeug HTTP response leak After a WebSocket handler exits, flask-sock returns a Response to Werkzeug which writes "HTTP/1.1 200 OK..." on the still-open socket. Browsers see these HTTP bytes as a malformed WebSocket frame, causing "Invalid frame header". Now the handler explicitly closes the raw TCP socket after the WebSocket close handshake, so Werkzeug's write harmlessly fails. Applied to both waterfall and audio WebSocket handlers. Co-Authored-By: Claude Opus 4.6 --- routes/audio_websocket.py | 20 ++++++++++++++++++-- routes/waterfall_websocket.py | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/routes/audio_websocket.py b/routes/audio_websocket.py index 6d70d0b..4e2acf5 100644 --- a/routes/audio_websocket.py +++ b/routes/audio_websocket.py @@ -1,10 +1,11 @@ """WebSocket-based audio streaming for SDR.""" +import json +import shutil +import socket import subprocess import threading import time -import shutil -import json from flask import Flask # Try to import flask-sock @@ -251,4 +252,19 @@ def init_audio_websocket(app: Flask): finally: with process_lock: kill_audio_processes() + # Complete WebSocket close handshake, then shut down the + # raw socket so Werkzeug cannot write its HTTP 200 response + # on top of the WebSocket stream. + try: + ws.close() + except Exception: + pass + try: + ws.sock.shutdown(socket.SHUT_RDWR) + except Exception: + pass + try: + ws.sock.close() + except Exception: + pass logger.info("WebSocket audio client disconnected") diff --git a/routes/waterfall_websocket.py b/routes/waterfall_websocket.py index 87a5fbc..c144f30 100644 --- a/routes/waterfall_websocket.py +++ b/routes/waterfall_websocket.py @@ -2,6 +2,7 @@ import json import queue +import socket import subprocess import threading import time @@ -348,4 +349,20 @@ def init_waterfall_websocket(app: Flask): unregister_process(iq_process) if claimed_device is not None: app_module.release_sdr_device(claimed_device) + # Complete WebSocket close handshake, then shut down the + # raw socket so Werkzeug cannot write its HTTP 200 response + # on top of the WebSocket stream (which browsers see as + # "Invalid frame header"). + try: + ws.close() + except Exception: + pass + try: + ws.sock.shutdown(socket.SHUT_RDWR) + except Exception: + pass + try: + ws.sock.close() + except Exception: + pass logger.info("WebSocket waterfall client disconnected")