diff --git a/backend/main.py b/backend/main.py index 76baed3..55c0c1f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1003,17 +1003,36 @@ async def serve_index(): return {"message": "Claude Persistent Agent API v3.0", "docs": "/docs"} -@app.get("/{full_path:path}") -async def serve_spa(full_path: str): - """Serve React SPA for all non-API routes. API and WS are handled above.""" - if full_path.startswith("api"): - raise HTTPException(404) - # Try serving the file directly (e.g. vite.svg, favicon) - file_path = STATIC_DIR / full_path - if file_path.is_file(): - return FileResponse(str(file_path)) - # Fall back to index.html for SPA routing - index = STATIC_DIR / "index.html" - if index.exists(): - return FileResponse(str(index)) - raise HTTPException(404) +@app.get("/vite.svg") +@app.get("/favicon.ico") +async def serve_static_root_files(): + """Serve known static root files.""" + from starlette.requests import Request + return FileResponse(str(STATIC_DIR / "vite.svg")) + + +# SPA fallback via middleware — this avoids the catch-all route problem +# that breaks WebSocket routing in some Starlette versions +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request as StarletteRequest +from starlette.responses import Response + + +class SPAFallbackMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: StarletteRequest, call_next): + response = await call_next(request) + # If a non-API GET request returned 404, serve index.html for SPA + if (response.status_code == 404 + and request.method == "GET" + and not request.url.path.startswith("/api") + and not request.url.path.startswith("/docs") + and not request.url.path.startswith("/openapi") + and not request.url.path.startswith("/assets") + and "websocket" not in request.headers.get("upgrade", "").lower()): + index = STATIC_DIR / "index.html" + if index.exists(): + return FileResponse(str(index)) + return response + + +app.add_middleware(SPAFallbackMiddleware)