{"record":{"id":"8f8896bc577ab63c","repo":"odysseus-dev/odysseus","slug":"referenced-upload-is-no-longer-available-missing-8f8896","errorCode":null,"errorMessage":"Referenced upload is no longer available: {missing_id}","messagePattern":"Referenced upload is no longer available: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"warning","filePath":"routes/history/history_routes.py","lineNumber":121,"sourceCode":"def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:\n    router = APIRouter(tags=[\"history\"])\n\n    def _reserve_message_uploads(\n        request: Request,\n        content: Any,\n        metadata: Any = None,\n    ) -> None:\n        try:\n            missing_id = reserve_message_upload_references(\n                upload_handler,\n                effective_user(request),\n                content,\n                metadata,\n            )\n        except (TypeError, ValueError) as exc:\n            raise HTTPException(400, \"Invalid message attachment metadata\") from exc\n        if missing_id:\n            raise HTTPException(\n                409,\n                f\"Referenced upload is no longer available: {missing_id}\",\n            )\n\n    def _db_history_entry(m: DbChatMessage) -> Dict[str, Any]:\n        entry = {\"role\": m.role, \"content\": _history_display_content(m.content)}\n        meta = {}\n        if m.meta_data:\n            try:\n                meta = json.loads(m.meta_data) or {}\n            except (json.JSONDecodeError, ValueError):\n                meta = {}\n        if m.timestamp and \"timestamp\" not in meta:\n            meta[\"timestamp\"] = m.timestamp.isoformat() + \"Z\"\n        if meta:\n            entry[\"metadata\"] = meta\n        return entry\n","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/history/history_routes.py#L103-L139","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Re-upload the attachment to get a fresh upload ID, then resend the message","Strip the stale attachment_id from metadata/content if the reference is no longer needed","If the file should still exist, verify it lies in the server's upload dir and is owned by the requesting user"],"exampleFix":"# client\nresp = post(f'/api/session/{sid}/message', json=payload)\nif resp.status_code == 409:\n    missing = resp.json().get('detail', '').rsplit(': ', 1)[-1]\n    payload['metadata']['attachments'] = [\n        a for a in payload['metadata'].get('attachments', [])\n        if a.get('attachment_id') != missing]\n    reupload_missing(); post(...)  # or resend without it","handlingStrategy":"fallback","validationCode":"def referenced_uploads_available(metadata) -> bool:\n    ids = {a['attachment_id'] for a in (metadata or {}).get('attachments', []) if a.get('attachment_id')}\n    for uid in ids:\n        r = requests.head(f'{base}/api/uploads/{uid}', headers=hdrs, timeout=15)\n        if r.status_code != 200:\n            return False\n    return True","typeGuard":null,"tryCatchPattern":"try:\n    post_message(sid, payload)\nexcept HTTPError as e:\n    if e.response.status_code == 409:\n        missing = e.response.json()['detail'].rsplit(': ', 1)[-1].strip()\n        payload = drop_reference(payload, missing)      # fall back: send without it\n        post_message(sid, payload)                      # then re-upload + re-send if needed\n    else:\n        raise","preventionTips":["Upload attachments and send the referencing message promptly — uploads expire after the retention window (cleanup_days)","After any upload failure or admin cleanup, purge matching attachment_ids from queued messages","Never reference upload IDs obtained under a different account; reservation is owner-scoped"],"tags":["http-409","conflict","attachments","uploads","lifecycle"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}