odysseus-dev/odysseus · error · HTTPException

Image update failed

Error message

Image update failed

What it means

HTTP 500 raised by POST /api/gallery/{image_id}/replace when the SQLAlchemy db.commit() fails after the new file bytes were already written to disk. The handler rolls back, logs 'gallery_replace: DB commit failed' with a traceback, and surfaces a generic 500. Root causes are DB-level: constraint violations, lock timeouts, dropped connections, or a dirty session.

Source

Thrown at routes/gallery/gallery_routes.py:471

            img_path = _gallery_image_path(img.filename)
            img_path.write_bytes(content)

            # Refresh dimensions in case the editor resized the canvas.
            # updated_at auto-bumps via TimestampMixin's onupdate hook.
            try:
                from PIL import Image
                from io import BytesIO
                with Image.open(BytesIO(content)) as new_im:
                    img.width = new_im.width
                    img.height = new_im.height
            except Exception:
                pass
            try:
                db.commit()
            except Exception:
                db.rollback()
                logger.exception("gallery_replace: DB commit failed")
                raise HTTPException(500, "Image update failed")
            return {"ok": True, "width": img.width, "height": img.height}
        finally:
            db.close()

    # ---- POST /api/gallery/{image_id}/rename ----
    @router.post("/api/gallery/{image_id}/rename")
    async def gallery_rename(request: Request, image_id: str):
        """Rename a gallery photo. Stores the new name in the `prompt`
        column (which serves as the user-facing label for uploaded
        photos that have no AI prompt)."""
        user = get_current_user(request)
        data = await request.json()
        new_name = (data.get("name") or "").strip()
        if not new_name:
            raise HTTPException(400, "Name cannot be empty")
        if len(new_name) > 500:
            raise HTTPException(400, "Name too long")
        db = SessionLocal()

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Read the server log for the 'gallery_replace: DB commit failed' traceback to get the real DB error.
  2. If SQLite, enable WAL or serialize writes to reduce 'database is locked'.
  3. If a constraint violation, inspect the GalleryImage row (width/height/prompt) for invalid values.
  4. Retry the request once after the transient condition clears; then re-check that the on-disk file matches the DB dimensions.

Example fix

// before: fire-and-forget
await replaceImage(id, fd);
// after: retry once on 500, then resync
try { await replaceImage(id, fd); }
catch (e) { if (e.status === 500) await replaceImage(id, fd); else throw e; }
Defensive patterns

Strategy: retry

Try / catch

try { await replaceImage(id, fd); }
catch (e) {
  if (e.status !== 500) throw e;
  await sleep(500);
  try { await replaceImage(id, fd); } // one retry for transient DB lock
  catch { await reloadImage(id); } // resync metadata with the already-written file
}

Prevention

When it happens

Trigger: Concurrent replaces on the same row hitting a lock timeout; DB restart or pool connection loss between the query and the commit; width/height assignment from a failed PIL parse leaving invalid state that violates a constraint.

Common situations: SQLite under concurrent writes (database is locked); MySQL/Postgres lock wait timeout; the new file on disk is now newer than the DB row (file updated but metadata stale) after this error.

Related errors


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