Loading noctua/app.py +30 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,36 @@ app.register_blueprint(api_blueprint, url_prefix='/api') app.register_blueprint(web_blueprint, url_prefix='/web') # Added Web Blueprint app.register_blueprint(viewer_blueprint, url_prefix='/api') class _WebSocketCloseRaceMiddleware: """Turns a known Quart teardown race into a clean log line. When a client abandons a WebSocket without a clean close handshake (page reload, tab close, network blip), Quart's own cancel_tasks() can end up sending a second 'websocket.close' after the first one already went out, and uvicorn raises RuntimeError for it. Harmless since switching off wsproto (the client still gets disconnected and noctua's ws-client.js reconnects on its own) but without this it surfaces as uvicorn's raw "ERROR: Exception in ASGI application" + full traceback, indistinguishable in the logs from an actual bug. """ def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): try: await self.app(scope, receive, send) except RuntimeError as e: if scope.get("type") == "websocket" and "after sending 'websocket.close'" in str(e): log.error(f"WebSocket: double-close race during teardown ({e})") return raise app = _WebSocketCloseRaceMiddleware(app) def _serve(ws): """Run uvicorn with the given websocket implementation until Ctrl-C.""" Loading Loading
noctua/app.py +30 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,36 @@ app.register_blueprint(api_blueprint, url_prefix='/api') app.register_blueprint(web_blueprint, url_prefix='/web') # Added Web Blueprint app.register_blueprint(viewer_blueprint, url_prefix='/api') class _WebSocketCloseRaceMiddleware: """Turns a known Quart teardown race into a clean log line. When a client abandons a WebSocket without a clean close handshake (page reload, tab close, network blip), Quart's own cancel_tasks() can end up sending a second 'websocket.close' after the first one already went out, and uvicorn raises RuntimeError for it. Harmless since switching off wsproto (the client still gets disconnected and noctua's ws-client.js reconnects on its own) but without this it surfaces as uvicorn's raw "ERROR: Exception in ASGI application" + full traceback, indistinguishable in the logs from an actual bug. """ def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): try: await self.app(scope, receive, send) except RuntimeError as e: if scope.get("type") == "websocket" and "after sending 'websocket.close'" in str(e): log.error(f"WebSocket: double-close race during teardown ({e})") return raise app = _WebSocketCloseRaceMiddleware(app) def _serve(ws): """Run uvicorn with the given websocket implementation until Ctrl-C.""" Loading