{"record":{"id":"61515e14eba627b9","repo":"odysseus-dev/odysseus","slug":"referenced-upload-is-no-longer-available-missing-61515e","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":"error","filePath":"routes/note/note_routes.py","lineNumber":603,"sourceCode":"\n    router = APIRouter(prefix=\"/api/notes\", tags=[\"notes\"])\n\n    def _owner(request: Request) -> Optional[str]:\n        # require_user, not bare get_current_user: a request that reaches\n        # these owner-scoped routes with NO identity (auth-middleware\n        # regression, SSRF from a sibling service) must fail closed (401)\n        # when auth is configured — not be treated as the single-user mode\n        # and handed blanket access to every account's notes. The documented\n        # anonymous modes (AUTH_ENABLED=false, LOCALHOST_BYPASS on loopback,\n        # unconfigured first-run) still resolve to None, the single-user\n        # path. fire_reminder below already gated this way; the CRUD routes\n        # did not.\n        return require_user(request) or None\n\n    def _reserve_note_uploads(owner: Optional[str], *values) -> None:\n        missing_id = reserve_upload_references(upload_handler, owner, *values)\n        if missing_id:\n            raise HTTPException(409, f\"Referenced upload is no longer available: {missing_id}\")\n\n    def _is_admin_or_single_user(request: Request, user: str | None) -> bool:\n        if user == INTERNAL_TOOL_USER:\n            return True\n        if not user:\n            # require_user() already admitted this request, which only happens\n            # for auth-disabled, loopback-bypass, or unconfigured single-user\n            # modes. There is no separate non-admin account boundary there.\n            return True\n        try:\n            from core.auth import AuthManager\n            auth_mgr = getattr(request.app.state, \"auth_manager\", None) or AuthManager()\n            if not getattr(auth_mgr, \"is_configured\", True):\n                return True\n            return bool(auth_mgr.is_admin(user))\n        except Exception:\n            return False\n","sourceCodeStart":585,"sourceCodeEnd":621,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/note/note_routes.py#L585-L621","documentation":"409 raised by _reserve_note_uploads (routes/note/note_routes.py:603) during note create/update when the note body references an upload id that reserve_upload_references reports as missing — i.e. the uploaded file was deleted or expired before the note was saved. It is an integrity guard: it prevents persisting a note pointing at a dead upload.","triggerScenarios":"Attaching an upload to a note, letting the upload expire/get garbage-collected (or deleting it via the uploads UI), then saving the note. Drafting a note with an image, upload TTL elapses, hitting save. Replaying an old create/update payload whose upload id was since purged.","commonSituations":"Upload retention windows shorter than typical note-draft lifetimes. Cleanup jobs removing orphaned uploads while a draft still references them. Client caching an upload id across sessions after server-side purge.","solutions":["Re-upload the file (or re-select a fresh upload) and save the note with the new upload id.","If the attachment is optional, strip the dead reference from image_url/content/items and save without it.","Check upload lifetime/retention config; raise the TTL if drafts regularly outlive uploads.","For the 409 path specifically, the message names the missing upload id — use it to find which field referenced it."],"exampleFix":"# before\nnote = {\"title\": t, \"image_url\": f\"/uploads/{OLD_ID}\"}  # OLD_ID purged -> 409\nclient.put(f\"/notes/{id}\", json=note)\n\n# after\nif not upload_exists(OLD_ID):\n    new_id = reupload_file(path)\n    note[\"image_url\"] = f\"/uploads/{new_id}\"\nclient.put(f\"/notes/{id}\", json=note)","handlingStrategy":"validation","validationCode":"for (const id of extractUploadIds(note)) {\n  if (!await api.uploadExists(id)) throw new Error(`upload ${id} gone; re-upload before saving`);\n}","typeGuard":null,"tryCatchPattern":"try { await api.updateNote(id, body); }\ncatch (e) {\n  if (e.status === 409 && /upload/i.test(e.message)) { await reuploadMissing(e.missingId); await api.updateNote(id, body); }\n  else throw e;\n}","preventionTips":["Save notes soon after attaching uploads; keep upload TTL well above draft lifetimes.","Strip dead upload references before saving rather than forcing them through.","Log the missing id from the 409 message to locate the offending field."],"tags":["fastapi","http-409","uploads","data-integrity","notes"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}