odysseus-dev/odysseus · error · HTTPException

Referenced upload is no longer available: {missing_id}

Error message

Referenced upload is no longer available: {missing_id}

What it means

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.

Source

Thrown at routes/note/note_routes.py:603

    router = APIRouter(prefix="/api/notes", tags=["notes"])

    def _owner(request: Request) -> Optional[str]:
        # require_user, not bare get_current_user: a request that reaches
        # these owner-scoped routes with NO identity (auth-middleware
        # regression, SSRF from a sibling service) must fail closed (401)
        # when auth is configured — not be treated as the single-user mode
        # and handed blanket access to every account's notes. The documented
        # anonymous modes (AUTH_ENABLED=false, LOCALHOST_BYPASS on loopback,
        # unconfigured first-run) still resolve to None, the single-user
        # path. fire_reminder below already gated this way; the CRUD routes
        # did not.
        return require_user(request) or None

    def _reserve_note_uploads(owner: Optional[str], *values) -> None:
        missing_id = reserve_upload_references(upload_handler, owner, *values)
        if missing_id:
            raise HTTPException(409, f"Referenced upload is no longer available: {missing_id}")

    def _is_admin_or_single_user(request: Request, user: str | None) -> bool:
        if user == INTERNAL_TOOL_USER:
            return True
        if not user:
            # require_user() already admitted this request, which only happens
            # for auth-disabled, loopback-bypass, or unconfigured single-user
            # modes. There is no separate non-admin account boundary there.
            return True
        try:
            from core.auth import AuthManager
            auth_mgr = getattr(request.app.state, "auth_manager", None) or AuthManager()
            if not getattr(auth_mgr, "is_configured", True):
                return True
            return bool(auth_mgr.is_admin(user))
        except Exception:
            return False

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Re-upload the file (or re-select a fresh upload) and save the note with the new upload id.
  2. If the attachment is optional, strip the dead reference from image_url/content/items and save without it.
  3. Check upload lifetime/retention config; raise the TTL if drafts regularly outlive uploads.
  4. For the 409 path specifically, the message names the missing upload id — use it to find which field referenced it.

Example fix

# before
note = {"title": t, "image_url": f"/uploads/{OLD_ID}"}  # OLD_ID purged -> 409
client.put(f"/notes/{id}", json=note)

# after
if not upload_exists(OLD_ID):
    new_id = reupload_file(path)
    note["image_url"] = f"/uploads/{new_id}"
client.put(f"/notes/{id}", json=note)
Defensive patterns

Strategy: validation

Validate before calling

for (const id of extractUploadIds(note)) {
  if (!await api.uploadExists(id)) throw new Error(`upload ${id} gone; re-upload before saving`);
}

Try / catch

try { await api.updateNote(id, body); }
catch (e) {
  if (e.status === 409 && /upload/i.test(e.message)) { await reuploadMissing(e.missingId); await api.updateNote(id, body); }
  else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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