{"record":{"id":"5384289a4525892c","repo":"odysseus-dev/odysseus","slug":"failed-to-list-calendars","errorCode":null,"errorMessage":"Failed to list calendars","messagePattern":"Failed to list calendars","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/calendar_routes.py","lineNumber":1135,"sourceCode":"    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            ]}\n        except HTTPException:\n            raise\n        except Exception as e:\n            db.rollback()\n            logger.error(\"Failed to list calendars: %s\", e)\n            raise HTTPException(500, \"Failed to list calendars\")\n        finally:\n            db.close()\n\n    @router.get(\"/events\")\n    async def list_events(request: Request, start: str, end: str, calendar: str = \"\"):\n        owner = _require_user(request)\n        try:\n            start_dt = _parse_dt(start)\n            end_dt = _parse_dt(end)\n        except ValueError:\n            # A malformed range (e.g. a stray \"NaN-NaN-NaN\" from the client)\n            # shouldn't spam the user with an error notification on every poll —\n            # just log it and return no events for this window.\n            logger.warning(\"list_events: unparseable range start=%r end=%r\", start, end)\n            return {\"events\": []}\n        db = SessionLocal()\n        try:\n            # Scope events to calendars owned by the caller.","sourceCodeStart":1117,"sourceCodeEnd":1153,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/calendar_routes.py#L1117-L1153","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the logged exception ('Failed to list calendars: <e>') to identify the true failure — insert race vs query error.","For an insert race on the default calendar, retry the GET once; after the winner commits, the loser's second attempt succeeds.","Apply pending schema migrations if the query itself is failing.","Reduce concurrent calendar polling or enable SQLite WAL to cut lock contention."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { return await api.get('/calendars'); }\ncatch (e) {\n  if (e.status === 500) return await api.get('/calendars'); // default-calendar insert race resolves on retry\n  throw e;\n}","preventionTips":["Retry GET /calendars once on 500 — the lazy default-calendar creation race is self-healing.","Avoid multiple tabs polling a brand-new account simultaneously.","Enable WAL on SQLite (or use a server DB) to reduce lock-induced 500s."],"tags":["database","http-500","fastapi","sqlalchemy","race-condition"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}