odysseus-dev/odysseus · error · HTTPException

Failed to list calendars

Error message

Failed to list calendars

What it means

Generic HTTP 500 from GET /calendars. The handler lazily ensures a default calendar exists (which commits), then queries all CalendarCal rows for the owner. Any exception outside HTTPException — default-calendar insert failing, query error, commit conflict — is rolled back, logged as 'Failed to list calendars: <e>', and returned as this 500.

Source

Thrown at routes/calendar_routes.py:1135

    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
            ]}
        except HTTPException:
            raise
        except Exception as e:
            db.rollback()
            logger.error("Failed to list calendars: %s", e)
            raise HTTPException(500, "Failed to list calendars")
        finally:
            db.close()

    @router.get("/events")
    async def list_events(request: Request, start: str, end: str, calendar: str = ""):
        owner = _require_user(request)
        try:
            start_dt = _parse_dt(start)
            end_dt = _parse_dt(end)
        except ValueError:
            # A malformed range (e.g. a stray "NaN-NaN-NaN" from the client)
            # shouldn't spam the user with an error notification on every poll —
            # just log it and return no events for this window.
            logger.warning("list_events: unparseable range start=%r end=%r", start, end)
            return {"events": []}
        db = SessionLocal()
        try:
            # Scope events to calendars owned by the caller.

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Read the logged exception ('Failed to list calendars: <e>') to identify the true failure — insert race vs query error.
  2. For an insert race on the default calendar, retry the GET once; after the winner commits, the loser's second attempt succeeds.
  3. Apply pending schema migrations if the query itself is failing.
  4. Reduce concurrent calendar polling or enable SQLite WAL to cut lock contention.
Defensive patterns

Strategy: retry

Try / catch

try { return await api.get('/calendars'); }
catch (e) {
  if (e.status === 500) return await api.get('/calendars'); // default-calendar insert race resolves on retry
  throw e;
}

Prevention

When it happens

Trigger: Unique-constraint violation on the lazily inserted default calendar when two concurrent GET /calendars requests race to create it; DB schema mismatch after an upgrade; database file locked or unavailable; corrupt owner column data.

Common situations: Two tabs polling /calendars on a fresh account simultaneously, both trying to insert the default calendar; migration not run so CalendarCal lacks a column the query references; SQLite contention with the CalDAV sync worker writing at the same moment.

Related errors


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