odysseus-dev/odysseus · warning · HTTPException

Invalid draft payload

Error message

Invalid draft payload: {exc}

What it means

Raised as HTTP 400 by POST /api/codex/emails/draft (and the send sibling) when the JSON body fails to construct routes.email_routes.SendEmailRequest — a pydantic-style request model. The exception text is appended verbatim, so the message enumerates exactly which fields failed validation (missing required fields, wrong types, unknown constraints).

Solutions

  1. Read the interpolated pydantic error in the message — it names the offending field(s); fix the payload accordingly.
  2. Fetch/inspect SendEmailRequest's current field list (routes/email_routes.py) and send all required fields with correct types.
  3. Ensure the request sends a JSON object with Content-Type: application/json.
  4. After an upgrade, re-check the model: renamed or newly-required fields are the usual breakers.

Example fix

# before
curl -X POST .../api/codex/emails/draft -d '{"body": "hello"}'  # 400 Invalid draft payload

# after
curl -X POST .../api/codex/emails/draft -H 'Content-Type: application/json' \
  -d '{"to": "a@b.com", "subject": "hi", "body": "hello"}'
Defensive patterns

Strategy: validation

Validate before calling

function validDraftPayload(b) {
  return !!b && typeof b === 'object'
    && typeof b.to === 'string' && b.to.includes('@')
    && typeof b.subject === 'string'
    && typeof b.body === 'string';
}
if (!validDraftPayload(body)) throw new Error('fix payload before sending');

Type guard

interface SendEmailRequest { to: string; subject: string; body: string }
function isSendEmailRequest(v: unknown): v is SendEmailRequest {
  const o = v as Record<string, unknown>;
  return typeof o?.to === 'string' && typeof o?.subject === 'string' && typeof o?.body === 'string';
}

Try / catch

try { r = await codexEmailDraft(body) } catch (e) { if (e.status === 400 && e.detail.startsWith('Invalid draft payload')) { showFieldErrors(e.detail) } else throw }

Prevention

When it happens

Trigger: POSTing a body missing required fields (e.g. no recipient/subject/content depending on the model), sending strings where the model expects other types, extra fields rejected by the model config, or sending a raw string body instead of a JSON object.

Common situations: Codex plugin or script submitting a partial draft (only 'body' text); client omits Content-Type: application/json so the body parses as garbage; field renames between versions (to -> recipient) breaking older clients.

Related errors


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

Appendix: source

Thrown at routes/codex_routes.py:373

        )
        result = await _as_owner(request, owner, documents_create_endpoint, request, req)
        if isinstance(result, dict):
            result = dict(result)
            result["draft_type"] = "document"
            result["send_required_confirmation"] = True
        return result

    @router.post("/emails/draft")
    async def codex_email_draft(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
        owner = _scope_owner(request, EMAIL_DRAFT_SCOPES)
        if email_draft_endpoint is None:
            raise HTTPException(503, "Email integration is not available")
        from routes.email_routes import SendEmailRequest

        try:
            req = SendEmailRequest(**body)
        except Exception as exc:
            raise HTTPException(400, f"Invalid draft payload: {exc}")
        return await email_draft_endpoint(req=req, owner=owner)

    @router.post("/emails/send")
    async def codex_email_send(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
        owner = _scope_owner(request, EMAIL_SEND_SCOPES)
        if email_send_endpoint is None:
            raise HTTPException(503, "Email integration is not available")
        from routes.email_routes import SendEmailRequest

        try:
            req = SendEmailRequest(**body)
        except Exception as exc:
            raise HTTPException(400, f"Invalid send payload: {exc}")
        return await email_send_endpoint(req=req, background_tasks=BackgroundTasks(), owner=owner)

    # ── Memory ────────────────────────────────────────────────────────────

    @router.get("/memory")

View on GitHub (pinned to f9235ebbf1)