{"record":{"id":"2788e6daa9ad99e6","repo":"odysseus-dev/odysseus","slug":"message-is-required","errorCode":null,"errorMessage":"Message is required","messagePattern":"Message is required","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/webhook/webhook_routes.py","lineNumber":252,"sourceCode":"        base_url: Optional[str] = Field(None, max_length=MAX_URL_LEN)\n        provider: Optional[str] = Field(None, max_length=50)\n\n    @router.post(\"/v1/chat\")\n    async def sync_chat(request: Request, body: SyncChatRequest):\n        if not getattr(request.state, \"api_token\", False):\n            raise HTTPException(403, \"This endpoint requires an API token\")\n        scopes = set(getattr(request.state, \"api_token_scopes\", []) or [])\n        if \"chat\" not in scopes:\n            raise HTTPException(403, \"API token is not scoped for chat\")\n        token_owner = getattr(request.state, \"api_token_owner\", None)\n\n        from core.models import ChatMessage\n        from src.llm_core import llm_call_async\n        from src.endpoint_resolver import build_chat_url, build_headers, build_models_url, normalize_base\n\n        message = body.message.strip()\n        if not message:\n            raise HTTPException(400, \"Message is required\")\n\n        session_id = body.session\n        sess = None\n\n        # --- Case 1: Resume an existing session ---\n        if session_id and session_manager:\n            try:\n                sess = session_manager.get_session(session_id)\n            except (KeyError, Exception):\n                raise HTTPException(404, \"Session not found\")\n            # SECURITY: verify the API-token's user owns this session — without\n            # this any token holder could resume any user's chat by passing its\n            # ID. The token's user is on request.state.user (set by API-token\n            # middleware); fall back to require_user if not present.\n            try:\n                from src.auth_helpers import get_current_user as _gcu\n                _tok_user = token_owner or getattr(request.state, \"user\", None) or _gcu(request)\n            except Exception:","sourceCodeStart":234,"sourceCodeEnd":270,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/webhook/webhook_routes.py#L234-L270","documentation":"HTTP 400 from POST /v1/chat: body.message is empty after strip(). The Pydantic field requires the message key, but a whitespace-only string passes the schema and is only caught by this explicit check.","triggerScenarios":"Posting {\"message\": \"\"} or {\"message\": \"   \\n\"}; an automation template whose message variable rendered empty (e.g. n8n expression resolved to blank).","commonSituations":"Workflow templates with unfilled placeholders; upstream LLM/step producing an empty string that gets forwarded; JSON forms without required-field validation.","solutions":["Send a non-empty message after trimming whitespace","In n8n/Make, add an IF node that skips/branches when the message expression is empty","Validate on the client before POSTing"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"msg = (body.get(\"message\") or \"\").strip()\nif not msg:\n    skip_or_branch()  # do not POST","typeGuard":"def is_sendable_message(v: unknown) -> bool:\n    return isinstance(v, str) and len(v.strip()) > 0","tryCatchPattern":null,"preventionTips":["Trim and check the message in the automation node before the HTTP call","Add an IF/branch on empty expressions in n8n/Make templates"],"tags":["validation","chat","http-400","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}