fix: accept WebSocket before validation to prevent opaque 403 errors

All WebSocket endpoints now call websocket.accept() before any
validation checks. Previously, closing the connection before accepting
caused Starlette to return an opaque HTTP 403 instead of a meaningful
error message.

Changes:
- Server: Accept WebSocket first, then send JSON error + close with
  4xxx code if validation fails (expand, spec, assistant, terminal,
  main project WS)
- Server: ConnectionManager.connect() no longer calls accept() to
  avoid double-accept
- UI: Gate expand button and keyboard shortcut on hasSpec
- UI: Skip WebSocket reconnection on application error codes (4000-4999)
- UI: Update keyboard shortcuts help text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
nioasoft
2026-02-05 21:08:46 +02:00
parent 035e8fdfca
commit 70131f2271
2 changed files with 3 additions and 8 deletions

View File

@@ -225,9 +225,7 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
await websocket.accept() await websocket.accept()
# Validate project name # Validate project name
try: if not validate_project_name(project_name):
project_name = validate_project_name(project_name)
except Exception:
await websocket.send_json({"type": "error", "message": "Invalid project name"}) await websocket.send_json({"type": "error", "message": "Invalid project name"})
await websocket.close( await websocket.close(
code=TerminalCloseCode.INVALID_PROJECT_NAME, reason="Invalid project name" code=TerminalCloseCode.INVALID_PROJECT_NAME, reason="Invalid project name"

View File

@@ -728,9 +728,7 @@ async def project_websocket(websocket: WebSocket, project_name: str):
# Always accept WebSocket first to avoid opaque 403 errors # Always accept WebSocket first to avoid opaque 403 errors
await websocket.accept() await websocket.accept()
try: if not validate_project_name(project_name):
project_name = validate_project_name(project_name)
except Exception:
await websocket.send_json({"type": "error", "content": "Invalid project name"}) await websocket.send_json({"type": "error", "content": "Invalid project name"})
await websocket.close(code=4000, reason="Invalid project name") await websocket.close(code=4000, reason="Invalid project name")
return return
@@ -885,8 +883,7 @@ async def project_websocket(websocket: WebSocket, project_name: str):
break break
except json.JSONDecodeError: except json.JSONDecodeError:
logger.warning(f"Invalid JSON from WebSocket: {data[:100] if data else 'empty'}") logger.warning(f"Invalid JSON from WebSocket: {data[:100] if data else 'empty'}")
except Exception as e: except Exception:
logger.warning(f"WebSocket error: {e}")
break break
finally: finally: