{"record":{"id":"3d28008046be739d","repo":"odysseus-dev/odysseus","slug":"image-update-failed","errorCode":null,"errorMessage":"Image update failed","messagePattern":"Image update failed","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/gallery/gallery_routes.py","lineNumber":471,"sourceCode":"            img_path = _gallery_image_path(img.filename)\n            img_path.write_bytes(content)\n\n            # Refresh dimensions in case the editor resized the canvas.\n            # updated_at auto-bumps via TimestampMixin's onupdate hook.\n            try:\n                from PIL import Image\n                from io import BytesIO\n                with Image.open(BytesIO(content)) as new_im:\n                    img.width = new_im.width\n                    img.height = new_im.height\n            except Exception:\n                pass\n            try:\n                db.commit()\n            except Exception:\n                db.rollback()\n                logger.exception(\"gallery_replace: DB commit failed\")\n                raise HTTPException(500, \"Image update failed\")\n            return {\"ok\": True, \"width\": img.width, \"height\": img.height}\n        finally:\n            db.close()\n\n    # ---- POST /api/gallery/{image_id}/rename ----\n    @router.post(\"/api/gallery/{image_id}/rename\")\n    async def gallery_rename(request: Request, image_id: str):\n        \"\"\"Rename a gallery photo. Stores the new name in the `prompt`\n        column (which serves as the user-facing label for uploaded\n        photos that have no AI prompt).\"\"\"\n        user = get_current_user(request)\n        data = await request.json()\n        new_name = (data.get(\"name\") or \"\").strip()\n        if not new_name:\n            raise HTTPException(400, \"Name cannot be empty\")\n        if len(new_name) > 500:\n            raise HTTPException(400, \"Name too long\")\n        db = SessionLocal()","sourceCodeStart":453,"sourceCodeEnd":489,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/gallery/gallery_routes.py#L453-L489","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the server log for the 'gallery_replace: DB commit failed' traceback to get the real DB error.","If SQLite, enable WAL or serialize writes to reduce 'database is locked'.","If a constraint violation, inspect the GalleryImage row (width/height/prompt) for invalid values.","Retry the request once after the transient condition clears; then re-check that the on-disk file matches the DB dimensions."],"exampleFix":"// before: fire-and-forget\nawait replaceImage(id, fd);\n// after: retry once on 500, then resync\ntry { await replaceImage(id, fd); }\ncatch (e) { if (e.status === 500) await replaceImage(id, fd); else throw e; }","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await replaceImage(id, fd); }\ncatch (e) {\n  if (e.status !== 500) throw e;\n  await sleep(500);\n  try { await replaceImage(id, fd); } // one retry for transient DB lock\n  catch { await reloadImage(id); } // resync metadata with the already-written file\n}","preventionTips":["Keep DB writes short to avoid lock timeouts","Watch server logs for the 'DB commit failed' traceback","After a 500 replace, GET the image to verify disk/DB consistency"],"tags":["http","database","sqlalchemy","gallery","commit"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}