/users dossier endpoint 500s for every user - non-finite float (inf) breaks JSONResponse #83

Closed
opened 2026-07-06 06:34:33 +00:00 by coilysiren · 2 comments
Owner

Symptom

/preview/user.json?name=<username> returns HTTP 500 for every user (verified live on the rolled 7a6e68e pod for elizacorn and coilysiren, 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)

File "src/eco_mcp_app/http_app.py", line 244, in preview_user_json
    return JSONResponse(dossier)
...
ValueError: Out of range float values are not JSON compliant: inf

build_user_dossier (users.py) puts a non-finite float (inf - almost certainly a unit-price / rate like total/qty with qty==0 from an upstream exporter) into the dossier. Starlette's JSONResponse renders with allow_nan=False, so any inf/-inf/nan anywhere in the payload raises and the whole endpoint 500s.

Fix

  • Load-bearing: sanitize non-finite floats to null (recursive walk over the dossier dict/list) immediately before JSONResponse(dossier) at http_app.py:244. Covers whatever source produces it.
  • Nice-to-have: also guard the division in users.py that yields inf so the value is a meaningful None/0 rather than silently dropped.

Verify

  • curl -s -o /dev/null -w '%{http_code}' https://<host>/preview/user.json?name=<active-user> -> 200.
  • Add a pytest with a dossier fixture containing inf/nan asserting the endpoint returns 200 and finite JSON.
  • Files: 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.

## Symptom `/preview/user.json?name=<username>` returns **HTTP 500 for every user** (verified live on the rolled `7a6e68e` pod for `elizacorn` and `coilysiren`, 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) ``` File "src/eco_mcp_app/http_app.py", line 244, in preview_user_json return JSONResponse(dossier) ... ValueError: Out of range float values are not JSON compliant: inf ``` `build_user_dossier` (`users.py`) puts a non-finite float (`inf` - almost certainly a unit-price / rate like `total/qty` with `qty==0` from an upstream exporter) into the dossier. Starlette's `JSONResponse` renders with `allow_nan=False`, so any `inf`/`-inf`/`nan` anywhere in the payload raises and the whole endpoint 500s. ## Fix - **Load-bearing:** sanitize non-finite floats to `null` (recursive walk over the dossier dict/list) immediately before `JSONResponse(dossier)` at `http_app.py:244`. Covers whatever source produces it. - **Nice-to-have:** also guard the division in `users.py` that yields `inf` so the value is a meaningful `None`/`0` rather than silently dropped. ## Verify - `curl -s -o /dev/null -w '%{http_code}' https://<host>/preview/user.json?name=<active-user>` -> **200**. - Add a pytest with a dossier fixture containing `inf`/`nan` asserting the endpoint returns 200 and finite JSON. - Files: `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.
Owner

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-83 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-06T06:34:40Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

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)
  • Resolved: coilyco-gaming/eco-app#83 · branch issue-83 · driver claude · workflow direct-main
  • Run: engineer-claude-eco-app-83 · ward v0.413.0 · dispatched 2026-07-06T06:34:40Z
  • Comment thread: 0 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

## Symptom
`/preview/user.json?name=<username>` returns **HTTP 500 for every user** (verified live on the rolled `7a6e68e` pod for `elizacorn` and `coilysiren`, 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)
` ` `
File "src/eco_mcp_app/http_app.py", line 244, in preview_user_json
    return JSONResponse(dossier)
...
ValueError: Out of range float values are not JSON compliant: inf
` ` `
`build_user_dossier` (`users.py`) puts a non-finite float (`inf` - almost certainly a unit-price / rate like `total/qty` with `qty==0` from an upstream exporter) into the dossier. Starlette's `JSONResponse` renders with `allow_nan=False`, so any `inf`/`-inf`/`nan` anywhere in the payload raises and the whole endpoint 500s.

## Fix
- **Load-bearing:** sanitize non-finite floats to `null` (recursive walk over the dossier dict/list) immediately before `JSONResponse(dossier)` at `http_app.py:244`. Covers whatever source produces it.
- **Nice-to-have:** also guard the division in `users.py` that yields `inf` so the value is a meaningful `None`/`0` rather than silently dropped.

## Verify
- `curl -s -o /dev/null -w '%{http_code}' https://<host>/preview/user.json?name=<active-user>` -> **200**.
- Add a pytest with a dossier fixture containing `inf`/`nan` asserting the endpoint returns 200 and finite JSON.
- Files: `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.

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.413.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-eco-app-83` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-06T06:34:40Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **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). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-gaming/eco-app#83` · branch `issue-83` · driver `claude` · workflow `direct-main` - **Run:** `engineer-claude-eco-app-83` · ward `v0.413.0` · dispatched `2026-07-06T06:34:40Z` - **Comment thread:** 0 included in the pre-flight read, 0 stripped (ward's own automated comments). **Issue body as seeded:** ``` ## Symptom `/preview/user.json?name=<username>` returns **HTTP 500 for every user** (verified live on the rolled `7a6e68e` pod for `elizacorn` and `coilysiren`, 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) ` ` ` File "src/eco_mcp_app/http_app.py", line 244, in preview_user_json return JSONResponse(dossier) ... ValueError: Out of range float values are not JSON compliant: inf ` ` ` `build_user_dossier` (`users.py`) puts a non-finite float (`inf` - almost certainly a unit-price / rate like `total/qty` with `qty==0` from an upstream exporter) into the dossier. Starlette's `JSONResponse` renders with `allow_nan=False`, so any `inf`/`-inf`/`nan` anywhere in the payload raises and the whole endpoint 500s. ## Fix - **Load-bearing:** sanitize non-finite floats to `null` (recursive walk over the dossier dict/list) immediately before `JSONResponse(dossier)` at `http_app.py:244`. Covers whatever source produces it. - **Nice-to-have:** also guard the division in `users.py` that yields `inf` so the value is a meaningful `None`/`0` rather than silently dropped. ## Verify - `curl -s -o /dev/null -w '%{http_code}' https://<host>/preview/user.json?name=<active-user>` -> **200**. - Add a pytest with a dossier fixture containing `inf`/`nan` asserting the endpoint returns 200 and finite JSON. - Files: `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. ``` Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.413.0). </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-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_float in trades.py, where a CSV field of Infinity sails through float() 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.

WARD-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_float` in trades.py, where a CSV field of `Infinity` sails through `float()` 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.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/eco-app#83
No description provided.