odysseus-dev/odysseus · warning · HTTPException
Image not found
Error message
Image not found
What it means
Raised as HTTPException(404) during image ownership verification before serving a gallery file. The code looks up the GalleryImage row by filename; a row owned by a different user than the authenticated requester is deliberately returned as 404 so the endpoint does not leak which filenames exist.
Source
Thrown at app.py:517
@app.get("/api/generated-image/{filename}")
async def serve_generated_image(filename: str, request: Request):
"""Serve generated images from the data directory."""
img_path = resolve_generated_image_path(filename)
# SECURITY: filename is the only key, so anyone who knows / guesses a
# 12-hex content hash could pull another user's image bytes. Require
# auth and verify ownership via the gallery row (when one exists).
try:
from src.auth_helpers import get_current_user
from core.database import SessionLocal as _SL, GalleryImage as _GI
_user = get_current_user(request)
if _user:
_db = _SL()
try:
_row = _db.query(_GI).filter(_GI.filename == filename).first()
# Generated-but-not-yet-imported images have no row → allow.
# Row exists with a different owner → 404 (don't confirm existence).
if _row is not None and _row.owner and _row.owner != _user:
raise HTTPException(status_code=404, detail="Image not found")
finally:
_db.close()
except HTTPException:
raise
except Exception as _e:
logger.warning("Image ownership verification failed for %r", filename, exc_info=_e)
ext = filename.rsplit('.', 1)[-1].lower()
mime = {
"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
"webp": "image/webp", "gif": "image/gif",
"mp4": "video/mp4", "mov": "video/quicktime", "webm": "video/webm",
"mkv": "video/x-matroska", "m4v": "video/mp4",
}.get(ext, "application/octet-stream")
# Generated-image filenames are content hashes → the bytes for a given
# filename never change. Cache them hard so the gallery doesn't
# re-download every full-size image each time it's opened. `immutable`
# tells the browser it never needs to revalidate within the max-age.
return FileResponse(View on GitHub (pinned to f9235ebbf1)
Solutions
- Access the image while authenticated as its owner
- If ownership looks wrong, correct the GalleryImage.owner row in the database
- Do not rely on this path for secrecy of unimported files — filenames without rows are served; import or protect them separately
Defensive patterns
Strategy: validation
Validate before calling
# Server-side pre-check before rendering links
row = db.query(GalleryImage).filter(GalleryImage.filename == fn).first()
if row is not None and row.owner and row.owner != current_user:
continue # skip linking images the user cannot fetch Try / catch
# Caller of the image endpoint
resp = requests.get(url, auth=...)
if resp.status_code == 404:
raise FileNotFoundError(filename) # treat as inaccessible Prevention
- Only generate image URLs for rows the requester owns
- Treat 404 as 'not yours or not exists' — do not retry with other users' credentials
- Import generated images promptly so ownership is recorded
When it happens
Trigger: Requesting /image/{filename} (or equivalent) while authenticated as user A for an image whose gallery row's owner is user B. Also note the guard only raises when the row exists with a different non-null owner; no-row (generated but unimported) is allowed through.
Common situations: Shared or guessed filenames across multi-user deployments; a user re-logging as a different account while the browser still requests another user's image; ownership fields changed by an import/migration.
Related errors
- Failed to update task
- Not your image
- Action '{task.action}' requires admin privileges
- data?.error || data?.detail || `HTTP ${res.status}`
- (data && (data.error || data.detail)) || `HTTP ${res.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/a21fce4835fe9b26.
Report an issue: GitHub.