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

  1. Check the server log line 'Failed to delete calendar <cal_id>: <e>' — the underlying exception text names the real cause.
  2. 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.
  3. If it is a missing table/column, run the app's schema migration or recreate the DB.
  4. 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

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


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