odysseus-dev/odysseus · warning · HTTPException

Invalid draft payload: {exc}

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).

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)

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.