fix: resolve /api/lab/result 500 and /api/lab/precons/install-batch 400

- GET /api/lab/result: Starlette's JSONResponse uses allow_nan=False,
  so NaN/Infinity floats from simulation output cause a ValueError crash.
  Add _sanitize_floats() to replace non-finite floats with null before
  returning. Also wrap json.load in try/except for better error reporting.

- POST /api/lab/precons/install-batch: Frontend sends {"names": [...]}
  but backend expected {"fileNames": [...]}. Accept both field names.

- POST /api/lab/precons/install: Same mismatch — frontend sends {"name"}
  but backend expected {"fileName"}. Accept both field names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 4.6 committed 2026-03-18 06:11:14 +00:00
1 parent 5550c27744
commit 71c90a588f
2 files changed
+24 -6

No files matched your search

+20 -2
View File
@@ -19,6 +19,7 @@ from __future__ import annotations
import glob
import json
import math
import os
import re
import threading
@@ -52,6 +53,19 @@ from routes.shared import (
router = APIRouter(tags=["lab"])
def _sanitize_floats(obj):
"""Replace NaN/Infinity with None so JSONResponse (allow_nan=False) won't crash."""
if isinstance(obj, float):
if math.isnan(obj) or math.isinf(obj):
return None
return obj
if isinstance(obj, dict):
return {k: _sanitize_floats(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_sanitize_floats(v) for v in obj]
return obj
@router.post("/api/lab/start", response_model=StartResponse)
async def start_batch(req: StartRequest, background_tasks: BackgroundTasks):
"""Start a new batch simulation run."""
@@ -162,8 +176,12 @@ async def get_result(batchId: Optional[str] = None):
raise HTTPException(500, state.error)
if not state.result_path or not os.path.exists(state.result_path):
raise HTTPException(500, "Result file not found")
with open(state.result_path, "r", encoding="utf-8") as f:
data = json.load(f)
try:
with open(state.result_path, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError) as e:
raise HTTPException(500, f"Failed to read result file: {e}")
data = _sanitize_floats(data)
return JSONResponse(content=data)
+4 -4
View File
@@ -31,9 +31,9 @@ async def list_precons():
@router.post("/api/lab/precons/install")
async def install_precon(req: dict):
"""Install a precon deck to the Forge decks directory.
Body: {"fileName": "Elven_Empire.dck"}
Body: {"fileName": "Elven_Empire.dck"} or {"name": "Elven_Empire.dck"}
"""
file_name = req.get("fileName", "")
file_name = req.get("fileName", "") or req.get("name", "")
if not file_name:
raise HTTPException(400, "fileName is required")
@@ -60,9 +60,9 @@ async def install_precon(req: dict):
@router.post("/api/lab/precons/install-batch")
async def install_precons_batch(req: dict):
"""Install multiple precon decks at once.
Body: {"fileNames": ["Elven_Empire.dck", "Necron_Dynasties.dck"]}
Body: {"fileNames": ["Elven_Empire.dck", ...]} or {"names": ["Elven_Empire.dck", ...]}
"""
file_names = req.get("fileNames", [])
file_names = req.get("fileNames", []) or req.get("names", [])
if not file_names:
raise HTTPException(400, "fileNames list is required")