mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-17 02:43:09 +00:00
fix: accept WebSocket before validation to prevent opaque 403 errors
All 5 WebSocket endpoints (expand, spec, assistant, terminal, project) were closing the connection before calling accept() when validation failed. Starlette converts pre-accept close into an HTTP 403, giving clients no meaningful error information. Server changes: - Move websocket.accept() before all validation checks in every WS handler - Send JSON error message before closing so clients get actionable errors - Fix validate_project_name usage (raises HTTPException, not returns bool) - ConnectionManager.connect() no longer calls accept() (caller's job) Client changes: - All 3 WS hooks (useWebSocket, useExpandChat, useSpecChat) skip reconnection on 4xxx close codes (application errors won't self-resolve) - Gate expand button, keyboard shortcut, and modal on hasSpec - Add hasSpec to useEffect dependency array to prevent stale closure - Update keyboard shortcuts help text for E key context Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -221,8 +221,14 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
||||
- {"type": "pong"} - Keep-alive response
|
||||
- {"type": "error", "message": "..."} - Error message
|
||||
"""
|
||||
# Always accept WebSocket first to avoid opaque 403 errors
|
||||
await websocket.accept()
|
||||
|
||||
# Validate project name
|
||||
if not validate_project_name(project_name):
|
||||
try:
|
||||
project_name = validate_project_name(project_name)
|
||||
except Exception:
|
||||
await websocket.send_json({"type": "error", "message": "Invalid project name"})
|
||||
await websocket.close(
|
||||
code=TerminalCloseCode.INVALID_PROJECT_NAME, reason="Invalid project name"
|
||||
)
|
||||
@@ -230,6 +236,7 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
||||
|
||||
# Validate terminal ID
|
||||
if not validate_terminal_id(terminal_id):
|
||||
await websocket.send_json({"type": "error", "message": "Invalid terminal ID"})
|
||||
await websocket.close(
|
||||
code=TerminalCloseCode.INVALID_PROJECT_NAME, reason="Invalid terminal ID"
|
||||
)
|
||||
@@ -238,6 +245,7 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
||||
# Look up project directory from registry
|
||||
project_dir = _get_project_path(project_name)
|
||||
if not project_dir:
|
||||
await websocket.send_json({"type": "error", "message": "Project not found in registry"})
|
||||
await websocket.close(
|
||||
code=TerminalCloseCode.PROJECT_NOT_FOUND,
|
||||
reason="Project not found in registry",
|
||||
@@ -245,6 +253,7 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
||||
return
|
||||
|
||||
if not project_dir.exists():
|
||||
await websocket.send_json({"type": "error", "message": "Project directory not found"})
|
||||
await websocket.close(
|
||||
code=TerminalCloseCode.PROJECT_NOT_FOUND,
|
||||
reason="Project directory not found",
|
||||
@@ -254,14 +263,13 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
||||
# Verify terminal exists in metadata
|
||||
terminal_info = get_terminal_info(project_name, terminal_id)
|
||||
if not terminal_info:
|
||||
await websocket.send_json({"type": "error", "message": "Terminal not found"})
|
||||
await websocket.close(
|
||||
code=TerminalCloseCode.PROJECT_NOT_FOUND,
|
||||
reason="Terminal not found",
|
||||
)
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
# Get or create terminal session for this project/terminal
|
||||
session = get_terminal_session(project_name, project_dir, terminal_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user