odysseus-dev/odysseus · warning · HTTPException

content is required

Error message

content is required

What it means

HTTP 400 from POST /api/session/{session_id}/message: the body's 'content' field must be a non-empty string after defaulting to ''. Empty message text is rejected before upload-reference reservation or ChatMessage creation. role defaults to 'assistant' and is not validated here.

Source

Thrown at routes/history/history_routes.py:270

            keep_count = body.get("keep_count", 0)
            result = session_manager.truncate_messages(session_id, keep_count)
            return {"status": "ok", "kept": keep_count, "truncated": result}
        except KeyError:
            raise HTTPException(404, "Session not found")
        except Exception as e:
            logger.error(f"Truncate error {session_id}: {e}")
            raise HTTPException(500, str(e))

    @router.post("/api/session/{session_id}/message")
    async def add_message(request: Request, session_id: str):
        """Add a message to a session (for slash command persistence)."""
        _verify_session_owner(request, session_id)
        try:
            body = await request.json()
            role = body.get("role", "assistant")
            content = body.get("content", "")
            if not content:
                raise HTTPException(400, "content is required")
            metadata = body.get("metadata")
            _reserve_message_uploads(request, content, metadata)
            msg = ChatMessage(role=role, content=content, metadata=metadata)
            session_manager.add_message(session_id, msg)
            return {"status": "ok"}
        except KeyError:
            raise HTTPException(404, "Session not found")

    @router.post("/api/session/{session_id}/delete-messages")
    async def delete_messages(request: Request, session_id: str):
        """Delete specific messages by DB ID (or legacy index)."""
        _verify_session_owner(request, session_id)
        try:
            body = await request.json()
            msg_ids = body.get("msg_ids", [])
            indices = body.get("indices")  # legacy fallback

            session = session_manager.get_session(session_id)

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Include non-empty content: {"role": "user", "content": "hello"}
  2. Skip the API call entirely when the message body is empty (guard client-side)
  3. If persisting slash-command output, check the result is non-empty before posting

Example fix

# before
if True: post(f'/api/session/{sid}/message', json={'role':'user','content': text})

# after
if text and text.strip():
    post(f'/api/session/{sid}/message', json={'role': 'user', 'content': text})
Defensive patterns

Strategy: validation

Validate before calling

def valid_message_body(body: dict) -> bool:
    content = body.get('content', '')
    return isinstance(content, str) and bool(content.strip())

Type guard

function isValidMessage(b: unknown): b is { role: string; content: string } {
  return typeof (b as any)?.content === 'string' && (b as any).content.trim().length > 0;
}

Prevention

When it happens

Trigger: POST with {"role": "user"} (no content), {"content": ""}, or {"content": null}; slash-command persistence layer firing with an empty command result.

Common situations: UI allowing send of whitespace-only or aborted messages; slash command producing empty output being persisted anyway; script echoing empty strings for assistant turns.

Related errors


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