/users dossier endpoint 500s for every user - non-finite float (inf) breaks JSONResponse #83
Labels
No labels
burndown-2026-06
burndown-2026-08
headless
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/eco-app#83
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
/preview/user.json?name=<username>returns HTTP 500 for every user (verified live on the rolled7a6e68epod forelizacornandcoilysiren, both active players). The hidden/users/<hex>SPA page (eco-app#80) therefore shows "dossier unavailable right now" for everyone.Root cause (traceback from the live pod)
build_user_dossier(users.py) puts a non-finite float (inf- almost certainly a unit-price / rate liketotal/qtywithqty==0from an upstream exporter) into the dossier. Starlette'sJSONResponserenders withallow_nan=False, so anyinf/-inf/nananywhere in the payload raises and the whole endpoint 500s.Fix
null(recursive walk over the dossier dict/list) immediately beforeJSONResponse(dossier)athttp_app.py:244. Covers whatever source produces it.users.pythat yieldsinfso the value is a meaningfulNone/0rather than silently dropped.Verify
curl -s -o /dev/null -w '%{http_code}' https://<host>/preview/user.json?name=<active-user>-> 200.inf/nanasserting the endpoint returns 200 and finite JSON.src/eco_mcp_app/http_app.py(~line 244),src/eco_mcp_app/users.py,tests/.Web service only - no game restart. Ships on the next eco-app roll.
🔒 Reserved by
ward agent --driver claude— containerengineer-claude-eco-app-83on hostKAI-DESKTOP-TOWERis carrying this issue (reserved 2026-07-06T06:34:40Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).
run seed context — what this run is carrying (ward#609)
coilyco-gaming/eco-app#83· branchissue-83· driverclaude· workflowdirect-mainengineer-claude-eco-app-83· wardv0.413.0· dispatched2026-07-06T06:34:40ZIssue body as seeded:
Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.413.0).
— Claude (she/her), via
ward agentWARD-OUTCOME: done - recursive non-finite sanitizer before JSONResponse (plus a trades-parser guard) landed on main as
13f4fc4, closing the /users dossier 500.Straightforward one in the end, and the issue's own diagnosis pointed the way. The load-bearing fix was exactly as described: a recursive walk dropping inf/-inf/nan to null right before
JSONResponse(dossier), since Starlette renders with allow_nan=False and one bad leaf sinks the whole payload.The one wrinkle was the 'nice-to-have guard in users.py' - there's actually no division there. The real ingest point is
pick_floatin trades.py, where a CSV field ofInfinitysails throughfloat()without raising (it only catches ValueError) and then feeds the unit-price division. So I guarded it there instead, coercing non-finite to 0.0, and noted the relocation in the commit. That felt like the honest fix rather than pattern-matching the issue text.Confidence is high: 489 tests green, lint/mypy/precommit clean, and I added coverage at all three layers (the sanitizer itself, the endpoint under a poisoned dossier, and the trades ingest guard). The sanitizer is defense-in-depth so it holds regardless of which exporter produces the inf. No live pod to curl from in here, so the 200 is asserted via TestClient rather than against the rolled service - worth a quick check after the next roll. Possible follow-up: apply the same sanitizer to any other JSONResponse endpoint that serializes exporter-derived floats, if we want the guarantee fleet-wide.