{"record":{"id":"3054707c03008d40","repo":"odysseus-dev/odysseus","slug":"invalid-send-payload-exc","errorCode":null,"errorMessage":"Invalid send payload: {exc}","messagePattern":"Invalid send payload: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/codex_routes.py","lineNumber":386,"sourceCode":"        from routes.email_routes import SendEmailRequest\n\n        try:\n            req = SendEmailRequest(**body)\n        except Exception as exc:\n            raise HTTPException(400, f\"Invalid draft payload: {exc}\")\n        return await email_draft_endpoint(req=req, owner=owner)\n\n    @router.post(\"/emails/send\")\n    async def codex_email_send(request: Request, body: dict[str, Any] = Body(default_factory=dict)):\n        owner = _scope_owner(request, EMAIL_SEND_SCOPES)\n        if email_send_endpoint is None:\n            raise HTTPException(503, \"Email integration is not available\")\n        from routes.email_routes import SendEmailRequest\n\n        try:\n            req = SendEmailRequest(**body)\n        except Exception as exc:\n            raise HTTPException(400, f\"Invalid send payload: {exc}\")\n        return await email_send_endpoint(req=req, background_tasks=BackgroundTasks(), owner=owner)\n\n    # ── Memory ────────────────────────────────────────────────────────────\n\n    @router.get(\"/memory\")\n    async def codex_memory_list(request: Request):\n        owner = _scope_owner(request, MEMORY_READ_SCOPES)\n        if memory_list_endpoint is None:\n            raise HTTPException(503, \"Memory integration is not available\")\n        return await _as_owner(request, owner, memory_list_endpoint, request)\n\n    @router.post(\"/memory\")\n    async def codex_memory_add(request: Request, body: dict[str, Any] = Body(default_factory=dict)):\n        owner = _scope_owner(request, MEMORY_WRITE_SCOPES)\n        if memory_add_endpoint is None:\n            raise HTTPException(503, \"Memory integration is not available\")\n        from src.request_models import MemoryAddRequest\n","sourceCodeStart":368,"sourceCodeEnd":404,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/codex_routes.py#L368-L404","documentation":"Raised by the Codex email proxy POST /api/codex/emails/send when the raw JSON body fails to construct routes.email_routes.SendEmailRequest. That Pydantic model requires the fields to, subject, and body (cc, bcc, body_html, attachments, account_id, in_reply_to, references, source_uid, source_folder are optional), so any missing required field, wrong type, or extra/misspelled key surfaces as ValidationError and is re-raised as HTTP 400 with the Pydantic message embedded.","triggerScenarios":"POST /api/codex/emails/send with a body missing 'to', 'subject', or 'body'; passing 'recipient' instead of 'to'; sending 'to' as a list when the model declares str; or any nested object the model cannot coerce.","commonSituations":"An LLM agent or script composing the payload by hand guesses field names; a UI refactor renames compose fields; sending HTML-only mail and forgetting the plain-text 'body' fallback; copying a payload from a different mail API (e.g. 'html' instead of 'body_html').","solutions":["Include all required fields: to (string), subject (string), body (plain-text string); optional fields are cc, bcc, body_html, in_reply_to, references, attachments, account_id, source_uid, source_folder.","Read the ValidationError text in the 400 response body — it names the exact offending field and reason.","Keep 'to' as a single comma/semicolon-separated string, matching the model's str type, not a JSON array.","Pre-validate locally with the same model: from routes.email_routes import SendEmailRequest; SendEmailRequest(**payload)."],"exampleFix":"// before\nfetch('/api/codex/emails/send', {method:'POST', body: JSON.stringify({recipient: 'a@b.c', subject: 'hi', body: 'hello'})})\n// after\nfetch('/api/codex/emails/send', {method:'POST', body: JSON.stringify({to: 'a@b.c', subject: 'hi', body: 'hello'})})","handlingStrategy":"validation","validationCode":"from routes.email_routes import SendEmailRequest\n\ndef valid_send_payload(body: dict) -> bool:\n    try:\n        SendEmailRequest(**body)\n        return True\n    except Exception:\n        return False\n\n# client-side (no imports): required string fields\n# ok = all(isinstance(body.get(k), str) and body[k] for k in ('to', 'subject', 'body'))","typeGuard":"def is_send_payload(b: dict) -> bool:\n    return (\n        isinstance(b, dict)\n        and isinstance(b.get('to'), str) and b['to'].strip()\n        and isinstance(b.get('subject'), str)\n        and isinstance(b.get('body'), str)\n    )","tryCatchPattern":"resp = requests.post(url, json=payload)\nif resp.status_code == 400 and 'Invalid send payload' in resp.text:\n    # parse the embedded pydantic message, fix fields, do NOT retry unchanged\n    show_validation_error(resp.json()['detail'])","preventionTips":["Build send payloads through one shared helper that always sets to/subject/body.","Keep 'to' a single string, not a list.","Add a pre-flight SendEmailRequest(**payload) check in agent code that composes mail."],"tags":["validation","email","fastapi","pydantic","codex-proxy"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}