1313
1414from flask import Blueprint , Response
1515
16- from api .flask_config import json_response
16+ from api .flask_config import api_error , json_response
1717
1818from utils .workspace_path import resolve_workspace_path
1919from utils .path_helpers import to_epoch_ms
@@ -28,6 +28,25 @@ def _read_json_file(path: str) -> Any:
2828 return json .load (f )
2929
3030
31+ def _parse_workspace_composer_rows (data : Any ) -> list [Any ]:
32+ """Validate ``composer.composerData`` envelope and return ``allComposers``."""
33+ if not isinstance (data , dict ):
34+ raise SchemaError (
35+ "WorkspaceComposers" ,
36+ "composer.composerData" ,
37+ hint = f"expected object, got { type (data ).__name__ } " ,
38+ )
39+ if "allComposers" not in data :
40+ raise SchemaError ("WorkspaceComposers" , "allComposers" )
41+ all_composers = data .get ("allComposers" )
42+ if not isinstance (all_composers , list ):
43+ raise SchemaError (
44+ "WorkspaceComposers" ,
45+ "allComposers" ,
46+ hint = f"expected list, got { type (all_composers ).__name__ } " ,
47+ )
48+ return all_composers
49+
3150@bp .route ("/api/composers" )
3251def list_composers () -> tuple [Response , int ] | Response :
3352 """List all composers across workspace databases (GET /api/composers).
@@ -67,22 +86,7 @@ def list_composers() -> tuple[Response, int] | Response:
6786 ).fetchone ()
6887
6988 if row and row [0 ]:
70- data = json .loads (row [0 ])
71- if not isinstance (data , dict ):
72- raise SchemaError (
73- "WorkspaceComposers" ,
74- "composer.composerData" ,
75- hint = f"expected object, got { type (data ).__name__ } " ,
76- )
77- if "allComposers" not in data :
78- raise SchemaError ("WorkspaceComposers" , "allComposers" )
79- all_composers = data .get ("allComposers" )
80- if not isinstance (all_composers , list ):
81- raise SchemaError (
82- "WorkspaceComposers" ,
83- "allComposers" ,
84- hint = f"expected list, got { type (all_composers ).__name__ } " ,
85- )
89+ all_composers = _parse_workspace_composer_rows (json .loads (row [0 ]))
8690 for c in all_composers :
8791 try :
8892 local = WorkspaceLocalComposer .from_dict (c )
@@ -126,7 +130,7 @@ def list_composers() -> tuple[Response, int] | Response:
126130
127131 except Exception :
128132 _logger .exception ("Failed to get composers" )
129- return json_response ({ "error" : " Failed to get composers"} , 500 )
133+ return api_error ( " Failed to get composers", "composers_list_failed" , 500 )
130134
131135
132136@bp .route ("/api/composers/<composer_id>" )
@@ -163,25 +167,7 @@ def get_composer(composer_id: str) -> tuple[Response, int] | Response:
163167 ).fetchone ()
164168
165169 if row and row [0 ]:
166- data = json .loads (row [0 ])
167- # Mirror the envelope guards list_composers() applies at line 60–74
168- # so a drifted local row (data not a dict, or allComposers missing
169- # / non-list) surfaces as a logged SchemaError, not a 500.
170- if not isinstance (data , dict ):
171- raise SchemaError (
172- "WorkspaceComposers" ,
173- "composer.composerData" ,
174- hint = f"expected object, got { type (data ).__name__ } " ,
175- )
176- if "allComposers" not in data :
177- raise SchemaError ("WorkspaceComposers" , "allComposers" )
178- all_composers = data .get ("allComposers" )
179- if not isinstance (all_composers , list ):
180- raise SchemaError (
181- "WorkspaceComposers" ,
182- "allComposers" ,
183- hint = f"expected list, got { type (all_composers ).__name__ } " ,
184- )
170+ all_composers = _parse_workspace_composer_rows (json .loads (row [0 ]))
185171 for c in all_composers :
186172 if isinstance (c , dict ) and c .get ("composerId" ) == composer_id :
187173 try :
@@ -240,14 +226,14 @@ def get_composer(composer_id: str) -> tuple[Response, int] | Response:
240226 e ,
241227 type (e ).__name__ ,
242228 )
243- return json_response ({ "error" : " Composer schema drift"} , 404 )
229+ return api_error ( " Composer schema drift", "composer_schema_drift" , 404 )
244230 payload = composer .cursor_storage_payload ()
245231 payload ["conversation" ] = payload .get ("conversation" ) or []
246232 return json_response (payload )
247233 except (OSError , sqlite3 .Error , json .JSONDecodeError , ValueError ):
248234 pass
249235
250- return json_response ({ "error" : " Composer not found"} , 404 )
236+ return api_error ( " Composer not found", "composer_not_found" , 404 )
251237 except Exception :
252238 _logger .exception ("Failed to get composer" )
253- return json_response ({ "error" : " Failed to get composer"} , 500 )
239+ return api_error ( " Failed to get composer", "composer_get_failed" , 500 )
0 commit comments