@@ -37,6 +37,32 @@ def require_state_segments(data: dict[str, Any], *, label: str) -> dict[str, Any
3737 return segs
3838
3939
40+ def request_json_object () -> dict [str , Any ]:
41+ """Return the JSON request body as an object. A missing body is ``{}``.
42+
43+ A JSON array, string, number, bool, or ``null`` raises :class:`WizardError`
44+ so handlers cannot ``.get`` on a list.
45+ """
46+ raw = request .get_json (silent = True )
47+ if raw is None :
48+ return {}
49+ if not isinstance (raw , dict ):
50+ raise WizardError (
51+ f"request body must be a JSON object, not { _json_kind (raw )} "
52+ )
53+ return raw
54+
55+
56+ def require_json_bool (data : dict [str , Any ], key : str , * , default : bool ) -> bool :
57+ """Return ``data[key]`` when present; reject non-bool JSON (including ``\" false\" ``)."""
58+ if key not in data or data [key ] is None :
59+ return default
60+ val = data [key ]
61+ if not isinstance (val , bool ):
62+ raise WizardError (f"{ key } must be a JSON boolean, not { _json_kind (val )} " )
63+ return val
64+
65+
4066def session_payload (config : Any | None ) -> dict [str , Any ]:
4167 """GUI session: frozen shell vs pip CLI, and whether a bundle is attached."""
4268 from docgen .resources import is_frozen
@@ -477,7 +503,10 @@ def api_session():
477503
478504 @app .route ("/api/open-bundle" , methods = ["POST" ])
479505 def api_open_bundle ():
480- data = request .get_json (silent = True ) or {}
506+ try :
507+ data = request_json_object ()
508+ except WizardError as exc :
509+ return jsonify ({"error" : str (exc )}), 400
481510 try :
482511 cfg = open_bundle_config (str (data .get ("path" ) or "" ))
483512 except ValueError as exc :
@@ -514,10 +543,13 @@ def api_tool_update():
514543 if blocked is not None :
515544 return blocked
516545 cfg = _cfg ()
517- data = request .json or {}
546+ try :
547+ data = request_json_object ()
548+ with_manim = require_json_bool (data , "with_manim" , default = False )
549+ update_req = require_json_bool (data , "update_requirements" , default = True )
550+ except WizardError as exc :
551+ return jsonify ({"error" : str (exc )}), 400
518552 ref = str (data .get ("ref" ) or "main" )
519- with_manim = bool (data .get ("with_manim" , False ))
520- update_req = bool (data .get ("update_requirements" , True ))
521553 bundle = cfg .base_dir if cfg else None
522554 try :
523555 result = update_docgen_install (
@@ -577,7 +609,10 @@ def api_generate_narration():
577609 if blocked is not None :
578610 return blocked
579611 cfg = _cfg ()
580- data = request .json or {}
612+ try :
613+ data = request_json_object ()
614+ except WizardError as exc :
615+ return jsonify ({"error" : str (exc )}), 400
581616 source_paths : list [str ] = list (data .get ("source_paths" ) or [])
582617 guidance : str = data .get ("guidance" , "" )
583618 segment_name : str = data .get ("segment_name" , "untitled" )
@@ -686,12 +721,8 @@ def api_get_state():
686721 def api_set_state ():
687722 cfg = _cfg ()
688723 base = cfg .base_dir if cfg else Path .cwd ()
689- raw = request .get_json (silent = True )
690- if raw is None :
691- raw = {}
692- if not isinstance (raw , dict ):
693- return jsonify ({"error" : "state must be a JSON object" }), 400
694724 try :
725+ raw = request_json_object ()
695726 segs = require_state_segments (raw , label = "state" )
696727 except WizardError as exc :
697728 return jsonify ({"error" : str (exc )}), 400
@@ -792,12 +823,15 @@ def api_put_focus(segment_id: str):
792823 cfg = _cfg ()
793824 if not cfg :
794825 return jsonify ({"error" : "no config" }), 400
795- data = request .json or {}
826+ try :
827+ data = request_json_object ()
828+ also_manim = require_json_bool (data , "also_manim" , default = True )
829+ do_yaml = require_json_bool (data , "yaml_generate" , default = True )
830+ except WizardError as exc :
831+ return jsonify ({"error" : str (exc )}), 400
796832 paths = data .get ("paths" )
797833 if not isinstance (paths , list ):
798834 return jsonify ({"error" : "paths must be a list of repo-root-relative strings" }), 400
799- also_manim = data .get ("also_manim" , True )
800- do_yaml = data .get ("yaml_generate" , True )
801835
802836 root = cfg .repo_root .resolve ()
803837 clean : list [str ] = []
@@ -879,7 +913,10 @@ def api_put_narration(segment_id: str):
879913 cfg = _cfg ()
880914 if not cfg :
881915 return jsonify ({"error" : "no config" }), 400
882- data = request .json or {}
916+ try :
917+ data = request_json_object ()
918+ except WizardError as exc :
919+ return jsonify ({"error" : str (exc )}), 400
883920 text = data .get ("text" , "" )
884921 seg_name = cfg .resolve_segment_name (segment_id )
885922 found = _find_asset (cfg .narration_dir , seg_name , segment_id , ".md" )
@@ -1056,8 +1093,11 @@ def api_run_from(step: str, segment_id: str):
10561093 cfg = _cfg ()
10571094 if not cfg :
10581095 return jsonify ({"error" : "no config" }), 400
1059- data = request .json or {}
1060- llm_scene = bool (data .get ("llm_scene_spec" , False ))
1096+ try :
1097+ data = request_json_object ()
1098+ llm_scene = require_json_bool (data , "llm_scene_spec" , default = False )
1099+ except WizardError as exc :
1100+ return jsonify ({"error" : str (exc )}), 400
10611101 from docgen .asset_graph import cascade_steps
10621102
10631103 try :
0 commit comments