odysseus-dev/odysseus · warning · HTTPException

Referenced upload is no longer available: {missing_id}

Error message

Referenced upload is no longer available: {missing_id}

What it means

HTTP 409 Conflict from _reserve_message_uploads: message content or metadata referenced an internal upload ID that the upload handler could not reserve — the file is gone, expired, owned by someone else, or the ID is invalid. The response names the exact offending missing_id. This prevents recording messages pointing at dead attachments.

Source

Thrown at routes/history/history_routes.py:121

def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
    router = APIRouter(tags=["history"])

    def _reserve_message_uploads(
        request: Request,
        content: Any,
        metadata: Any = None,
    ) -> None:
        try:
            missing_id = reserve_message_upload_references(
                upload_handler,
                effective_user(request),
                content,
                metadata,
            )
        except (TypeError, ValueError) as exc:
            raise HTTPException(400, "Invalid message attachment metadata") from exc
        if missing_id:
            raise HTTPException(
                409,
                f"Referenced upload is no longer available: {missing_id}",
            )

    def _db_history_entry(m: DbChatMessage) -> Dict[str, Any]:
        entry = {"role": m.role, "content": _history_display_content(m.content)}
        meta = {}
        if m.meta_data:
            try:
                meta = json.loads(m.meta_data) or {}
            except (json.JSONDecodeError, ValueError):
                meta = {}
        if m.timestamp and "timestamp" not in meta:
            meta["timestamp"] = m.timestamp.isoformat() + "Z"
        if meta:
            entry["metadata"] = meta
        return entry

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Re-upload the attachment to get a fresh upload ID, then resend the message
  2. Strip the stale attachment_id from metadata/content if the reference is no longer needed
  3. If the file should still exist, verify it lies in the server's upload dir and is owned by the requesting user

Example fix

# client
resp = post(f'/api/session/{sid}/message', json=payload)
if resp.status_code == 409:
    missing = resp.json().get('detail', '').rsplit(': ', 1)[-1]
    payload['metadata']['attachments'] = [
        a for a in payload['metadata'].get('attachments', [])
        if a.get('attachment_id') != missing]
    reupload_missing(); post(...)  # or resend without it
Defensive patterns

Strategy: fallback

Validate before calling

def referenced_uploads_available(metadata) -> bool:
    ids = {a['attachment_id'] for a in (metadata or {}).get('attachments', []) if a.get('attachment_id')}
    for uid in ids:
        r = requests.head(f'{base}/api/uploads/{uid}', headers=hdrs, timeout=15)
        if r.status_code != 200:
            return False
    return True

Try / catch

try:
    post_message(sid, payload)
except HTTPError as e:
    if e.response.status_code == 409:
        missing = e.response.json()['detail'].rsplit(': ', 1)[-1].strip()
        payload = drop_reference(payload, missing)      # fall back: send without it
        post_message(sid, payload)                      # then re-upload + re-send if needed
    else:
        raise

Prevention

When it happens

Trigger: POST a message whose content embeds an internal upload reference after the underlying file was deleted or aged out (cleanup_days=30 pruning), or whose metadata.attachment_id references another user's upload (reserve_upload with allow_admin=False rejects cross-owner).

Common situations: Uploads older than the 30-day retention window; file removed by an admin/cleanup job; copying message payloads between accounts; attachment ID typo'd or truncated by rich-text serialization.

Related errors


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