odysseus-dev/odysseus · error · HTTPException
Failed to delete calendar
Error message
Failed to delete calendar
What it means
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.
Source
Thrown at routes/calendar_routes.py:1111
return await sync_caldav_direction(owner, direction)
@router.delete("/calendars/{cal_id}")
async def delete_calendar(request: Request, cal_id: str):
owner = _require_user(request)
db = SessionLocal()
try:
cal = _get_or_404_calendar(db, cal_id, owner)
db.query(CalendarEvent).filter(CalendarEvent.calendar_id == cal_id).delete()
db.delete(cal)
db.commit()
return {"ok": True}
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error("Failed to delete calendar %s: %s", cal_id, e)
raise HTTPException(500, "Failed to delete calendar")
finally:
db.close()
@router.get("/calendars")
async def list_calendars(request: Request):
owner = _require_user(request)
db = SessionLocal()
try:
_ensure_default_calendar(db, owner)
# Listing calendars intentionally lazily creates a durable default.
# Other callers commit it with the event they are creating.
db.commit()
cals = db.query(CalendarCal).filter(CalendarCal.owner == owner).all()
return {"calendars": [
{"name": c.name, "href": c.id, "color": c.color, "source": c.source}
for c in cals
]}View on GitHub (pinned to f9235ebbf1)
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.
Defensive patterns
Strategy: try-catch
Validate before calling
// verify ownership/existence first to get the clean 404 instead of guessing
const cals = await (await fetch('/calendars')).json();
if (!cals.calendars.some(c => c.href === calId)) throw new NotFound(); Try / catch
try { await api.del(`/calendars/${calId}`); }
catch (e) {
if (e.status === 500) {
// check server log 'Failed to delete calendar <id>' for root cause
await sleep(500); await api.del(`/calendars/${calId}`); // one retry for transient locks
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to list calendars
- Failed to create calendar
- Failed to update calendar
- Failed to create document: {e}
- Failed to fetch document library: {e}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/9844f94267cf9aca.
Report an issue: GitHub.