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:
@@ -178,8 +178,8 @@ function App() {
|
||||
setShowAddFeature(true)
|
||||
}
|
||||
|
||||
// E : Expand project with AI (when project selected and has features)
|
||||
if ((e.key === 'e' || e.key === 'E') && selectedProject && features &&
|
||||
// E : Expand project with AI (when project selected, has spec and has features)
|
||||
if ((e.key === 'e' || e.key === 'E') && selectedProject && hasSpec && features &&
|
||||
(features.pending.length + features.in_progress.length + features.done.length) > 0) {
|
||||
e.preventDefault()
|
||||
setShowExpandProject(true)
|
||||
@@ -239,7 +239,7 @@ function App() {
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, debugActiveTab, assistantOpen, features, showSettings, showKeyboardHelp, isSpecCreating, viewMode, showResetModal, wsState.agentStatus])
|
||||
}, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, debugActiveTab, assistantOpen, features, showSettings, showKeyboardHelp, isSpecCreating, viewMode, showResetModal, wsState.agentStatus, hasSpec])
|
||||
|
||||
// Combine WebSocket progress with feature data
|
||||
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
||||
@@ -490,7 +490,7 @@ function App() {
|
||||
)}
|
||||
|
||||
{/* Expand Project Modal - AI-powered bulk feature creation */}
|
||||
{showExpandProject && selectedProject && (
|
||||
{showExpandProject && selectedProject && hasSpec && (
|
||||
<ExpandProjectModal
|
||||
isOpen={showExpandProject}
|
||||
projectName={selectedProject}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function KanbanBoard({ features, onFeatureClick, onAddFeature, onExpandPr
|
||||
onFeatureClick={onFeatureClick}
|
||||
onAddFeature={onAddFeature}
|
||||
onExpandProject={onExpandProject}
|
||||
showExpandButton={hasFeatures}
|
||||
showExpandButton={hasFeatures && hasSpec}
|
||||
onCreateSpec={onCreateSpec}
|
||||
showCreateSpec={!hasSpec && !hasFeatures}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,7 @@ const shortcuts: Shortcut[] = [
|
||||
{ key: 'D', description: 'Toggle debug panel' },
|
||||
{ key: 'T', description: 'Toggle terminal tab' },
|
||||
{ key: 'N', description: 'Add new feature', context: 'with project' },
|
||||
{ key: 'E', description: 'Expand project with AI', context: 'with features' },
|
||||
{ key: 'E', description: 'Expand project with AI', context: 'with spec & features' },
|
||||
{ key: 'A', description: 'Toggle AI assistant', context: 'with project' },
|
||||
{ key: 'G', description: 'Toggle Kanban/Graph view', context: 'with project' },
|
||||
{ key: ',', description: 'Open settings' },
|
||||
|
||||
@@ -107,16 +107,20 @@ export function useExpandChat({
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
ws.onclose = (event) => {
|
||||
setConnectionStatus('disconnected')
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current)
|
||||
pingIntervalRef.current = null
|
||||
}
|
||||
|
||||
// Don't retry on application-level errors (4xxx codes won't resolve on retry)
|
||||
const isAppError = event.code >= 4000 && event.code <= 4999
|
||||
|
||||
// Attempt reconnection if not intentionally closed
|
||||
if (
|
||||
!manuallyDisconnectedRef.current &&
|
||||
!isAppError &&
|
||||
reconnectAttempts.current < maxReconnectAttempts &&
|
||||
!isCompleteRef.current
|
||||
) {
|
||||
|
||||
@@ -157,15 +157,18 @@ export function useSpecChat({
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
ws.onclose = (event) => {
|
||||
setConnectionStatus('disconnected')
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current)
|
||||
pingIntervalRef.current = null
|
||||
}
|
||||
|
||||
// Don't retry on application-level errors (4xxx codes won't resolve on retry)
|
||||
const isAppError = event.code >= 4000 && event.code <= 4999
|
||||
|
||||
// Attempt reconnection if not intentionally closed
|
||||
if (reconnectAttempts.current < maxReconnectAttempts && !isCompleteRef.current) {
|
||||
if (!isAppError && reconnectAttempts.current < maxReconnectAttempts && !isCompleteRef.current) {
|
||||
reconnectAttempts.current++
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
|
||||
reconnectTimeoutRef.current = window.setTimeout(connect, delay)
|
||||
|
||||
@@ -335,10 +335,14 @@ export function useProjectWebSocket(projectName: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
ws.onclose = (event) => {
|
||||
setState(prev => ({ ...prev, isConnected: false }))
|
||||
wsRef.current = null
|
||||
|
||||
// Don't retry on application-level errors (4xxx codes won't resolve on retry)
|
||||
const isAppError = event.code >= 4000 && event.code <= 4999
|
||||
if (isAppError) return
|
||||
|
||||
// Exponential backoff reconnection
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000)
|
||||
reconnectAttempts.current++
|
||||
|
||||
Reference in New Issue
Block a user