adeshboudh16 commited on
Commit
b7b50cb
·
1 Parent(s): 08fce0d

fix(api): degrade gracefully when graph is unavailable

Browse files
src/civicsetu/api/main.py CHANGED
@@ -123,6 +123,13 @@ def create_app() -> FastAPI:
123
  app.include_router(query.router, prefix="/api/v1", tags=["query"])
124
  app.include_router(graph.router, prefix="/api/v1", tags=["graph"])
125
 
 
 
 
 
 
 
 
126
  return app
127
 
128
  app = create_app()
 
123
  app.include_router(query.router, prefix="/api/v1", tags=["query"])
124
  app.include_router(graph.router, prefix="/api/v1", tags=["graph"])
125
 
126
+ @app.get("/")
127
+ async def root():
128
+ return {
129
+ "message": "CivicSetu API is running. Frontend is served separately in this deployment.",
130
+ "status": "ok",
131
+ }
132
+
133
  return app
134
 
135
  app = create_app()
src/civicsetu/api/routes/graph.py CHANGED
@@ -31,6 +31,18 @@ router = APIRouter()
31
 
32
  _topo_cache: dict = {"data": None, "ts": 0.0}
33
  _TOPO_TTL = 300
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
 
36
  @router.get("/graph/topology", response_model=GraphTopologyResponse)
@@ -45,6 +57,12 @@ async def get_graph_topology() -> GraphTopologyResponse:
45
  nodes_raw, edges_raw = await GraphStore.get_topology()
46
  stats = await GraphStore.graph_stats()
47
  except Exception as e:
 
 
 
 
 
 
48
  log.error("topology_fetch_failed", error=str(e))
49
  raise HTTPException(status_code=500, detail=f"Graph topology fetch failed: {e}")
50
 
 
31
 
32
  _topo_cache: dict = {"data": None, "ts": 0.0}
33
  _TOPO_TTL = 300
34
+ _EMPTY_GRAPH_STATS = {
35
+ "docs": 0,
36
+ "sections": 0,
37
+ "refs": 0,
38
+ "has_sec": 0,
39
+ "derived_from": 0,
40
+ }
41
+
42
+
43
+ def _is_neo4j_auth_error(error: Exception) -> bool:
44
+ message = str(error)
45
+ return "Neo.ClientError.Security.Unauthorized" in message or "authentication failure" in message
46
 
47
 
48
  @router.get("/graph/topology", response_model=GraphTopologyResponse)
 
57
  nodes_raw, edges_raw = await GraphStore.get_topology()
58
  stats = await GraphStore.graph_stats()
59
  except Exception as e:
60
+ if _is_neo4j_auth_error(e):
61
+ log.warning("topology_fetch_degraded", reason="neo4j_auth_failed")
62
+ response = GraphTopologyResponse(nodes=[], edges=[], stats=_EMPTY_GRAPH_STATS)
63
+ _topo_cache["data"] = response
64
+ _topo_cache["ts"] = now
65
+ return response
66
  log.error("topology_fetch_failed", error=str(e))
67
  raise HTTPException(status_code=500, detail=f"Graph topology fetch failed: {e}")
68
 
tests/unit/api/test_query_route.py CHANGED
@@ -105,6 +105,41 @@ def test_app_startup_on_non_windows_does_not_shadow_asyncio():
105
  mock_get_ranker.assert_called_once()
106
 
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  def test_query_returns_200_with_citations(client):
109
  test_client, mock_graph = client
110
  mock_graph.invoke.return_value = {
 
105
  mock_get_ranker.assert_called_once()
106
 
107
 
108
+ def test_root_returns_ok_when_frontend_not_served(client):
109
+ test_client, _ = client
110
+
111
+ response = test_client.get("/")
112
+
113
+ assert response.status_code == 200
114
+ assert response.json()["status"] == "ok"
115
+
116
+
117
+ def test_graph_topology_returns_empty_payload_when_neo4j_auth_fails(client):
118
+ test_client, _ = client
119
+
120
+ with patch(
121
+ "civicsetu.api.routes.graph.GraphStore.get_topology",
122
+ new=AsyncMock(side_effect=RuntimeError("Neo.ClientError.Security.Unauthorized")),
123
+ ), patch(
124
+ "civicsetu.api.routes.graph.GraphStore.graph_stats",
125
+ new=AsyncMock(side_effect=RuntimeError("Neo.ClientError.Security.Unauthorized")),
126
+ ):
127
+ response = test_client.get("/api/v1/graph/topology")
128
+
129
+ assert response.status_code == 200
130
+ assert response.json() == {
131
+ "nodes": [],
132
+ "edges": [],
133
+ "stats": {
134
+ "docs": 0,
135
+ "sections": 0,
136
+ "refs": 0,
137
+ "has_sec": 0,
138
+ "derived_from": 0,
139
+ },
140
+ }
141
+
142
+
143
  def test_query_returns_200_with_citations(client):
144
  test_client, mock_graph = client
145
  mock_graph.invoke.return_value = {