odysseus-dev/odysseus · error · HTTPException

Invalid JSON

Error message

Invalid JSON: {e}

What it means

The streaming chat endpoint received a request with Content-Type application/json whose body failed to parse as JSON. json.JSONDecodeError is caught and re-raised as a 400 with the parser's message, so the exact syntax problem (position, error class) is included.

Solutions

  1. Validate the payload with JSON.parse (JS) or json.loads (Python) before sending
  2. Send with JSON.stringify(obj) / json= in requests, never a raw template string
  3. Check for proxy truncation if the error position equals the end of a shortened body

Example fix

# before
requests.post(url, data=str(payload), headers={'Content-Type':'application/json'})
# after
import json
requests.post(url, data=json.dumps(payload), headers={'Content-Type':'application/json'})
Defensive patterns

Strategy: validation

Validate before calling

const raw = JSON.stringify(payload);
try { JSON.parse(raw); } catch { fixSerialization(); return; }
await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: raw});

Try / catch

try { await send(); } catch (e) { if (e.status === 400 && e.message.startsWith('Invalid JSON')) { console.error('payload was not valid JSON', payload); } }

Prevention

When it happens

Trigger: POST /api/chat_stream with Content-Type: application/json and a malformed body: trailing commas, single quotes, unescaped newlines, truncated payload from a proxy or client bug.

Common situations: Hand-crafted curl requests with quoting mistakes; a proxy or LB truncating large bodies; client serializing form data as a string instead of JSON.stringify; encoding issues introducing BOM or control characters.

Understand the failure class

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/8c54c6691e8b9b72. Report an issue: GitHub.

Appendix: source

Thrown at routes/chat_routes.py:876

            "model": actual_model,
            "requested_endpoint_id": requested_route.get("endpoint_id"),
            "requested_endpoint_label": requested_route.get("endpoint_label"),
            "endpoint_id": actual_route.get("endpoint_id"),
            "endpoint_label": actual_route.get("endpoint_label"),
        }

    # ------------------------------------------------------------------ #
    # POST /api/chat_stream
    # ------------------------------------------------------------------ #
    @router.post("/api/chat_stream")
    async def chat_stream(request: Request) -> StreamingResponse:
        body = None
        try:
            if request.headers.get("content-type", "").startswith("application/json"):
                try:
                    body = await request.json()
                except json.JSONDecodeError as e:
                    raise HTTPException(400, f"Invalid JSON: {e}")
        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(400, f"Request parsing error: {e}")

        _set_user_time_from_request(request)

        form_data = await request.form()
        message = form_data.get("message")
        session = form_data.get("session")
        attachments = form_data.get("attachments")
        use_web = form_data.get("use_web")
        use_research = form_data.get("use_research")
        time_filter = form_data.get("time_filter")
        preset_id = form_data.get("preset_id")
        selected_endpoint_id = str(
            form_data.get("selected_endpoint_id")
            or (body or {}).get("selected_endpoint_id")

View on GitHub (pinned to f9235ebbf1)