{"record":{"id":"9844f94267cf9aca","repo":"odysseus-dev/odysseus","slug":"failed-to-delete-calendar","errorCode":null,"errorMessage":"Failed to delete calendar","messagePattern":"Failed to delete calendar","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/calendar_routes.py","lineNumber":1111,"sourceCode":"        return await sync_caldav_direction(owner, direction)\n\n\n    @router.delete(\"/calendars/{cal_id}\")\n    async def delete_calendar(request: Request, cal_id: str):\n        owner = _require_user(request)\n        db = SessionLocal()\n        try:\n            cal = _get_or_404_calendar(db, cal_id, owner)\n            db.query(CalendarEvent).filter(CalendarEvent.calendar_id == cal_id).delete()\n            db.delete(cal)\n            db.commit()\n            return {\"ok\": True}\n        except HTTPException:\n            raise\n        except Exception as e:\n            db.rollback()\n            logger.error(\"Failed to delete calendar %s: %s\", cal_id, e)\n            raise HTTPException(500, \"Failed to delete calendar\")\n        finally:\n            db.close()\n\n\n    @router.get(\"/calendars\")\n    async def list_calendars(request: Request):\n        owner = _require_user(request)\n        db = SessionLocal()\n        try:\n            _ensure_default_calendar(db, owner)\n            # Listing calendars intentionally lazily creates a durable default.\n            # Other callers commit it with the event they are creating.\n            db.commit()\n            cals = db.query(CalendarCal).filter(CalendarCal.owner == owner).all()\n            return {\"calendars\": [\n                {\"name\": c.name, \"href\": c.id, \"color\": c.color, \"source\": c.source}\n                for c in cals\n            ]}","sourceCodeStart":1093,"sourceCodeEnd":1129,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/calendar_routes.py#L1093-L1129","documentation":"Generic HTTP 500 from DELETE /calendars/{cal_id}. The handler deletes all CalendarEvent rows for the calendar, then the calendar itself, inside one transaction; any non-HTTP exception (database lock, constraint, connection drop) triggers rollback, is logged as 'Failed to delete calendar <id>: <e>', and surfaces as this opaque 500. HTTPException-derived errors (the 404 from _get_or_404_calendar) are re-raised untouched.","triggerScenarios":"SQLite database locked by a concurrent writer while the multi-row delete runs; DB file deleted or moved (OperationalError: no such table / unable to open); a foreign-key constraint from a table not cascaded in this handler; disk full at commit time.","commonSituations":"Another request (event create, CalDAV sync worker) holding a write lock on SQLite; app upgraded with a schema migration not applied so CalendarEvent or CalendarCal tables mismatch; long-running calendar with thousands of events timing out mid-delete.","solutions":["Check the server log line 'Failed to delete calendar <cal_id>: <e>' — the underlying exception text names the real cause.","If it is 'database is locked', reduce concurrent writes (retry after the other request finishes) or move to a server-side DB / enable WAL on SQLite.","If it is a missing table/column, run the app's schema migration or recreate the DB.","Confirm the calendar id exists and is owned by the caller first — a bad id normally gives 404, not this 500."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// verify ownership/existence first to get the clean 404 instead of guessing\nconst cals = await (await fetch('/calendars')).json();\nif (!cals.calendars.some(c => c.href === calId)) throw new NotFound();","typeGuard":null,"tryCatchPattern":"try { await api.del(`/calendars/${calId}`); }\ncatch (e) {\n  if (e.status === 500) {\n    // check server log 'Failed to delete calendar <id>' for root cause\n    await sleep(500); await api.del(`/calendars/${calId}`); // one retry for transient locks\n  } else throw e;\n}","preventionTips":["Treat calendar 500s as 'check the server log first' — the logged exception names the real cause.","Avoid firing calendar mutations while a CalDAV sync is running to dodge SQLite lock contention.","Keep schema migrations current so the cascade delete matches the live schema."],"tags":["database","http-500","fastapi","sqlalchemy","sqlite"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}