{"record":{"id":"be6ea05878a0515e","repo":"odysseus-dev/odysseus","slug":"failed-to-list-events","errorCode":null,"errorMessage":"Failed to list events","messagePattern":"Failed to list events","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/calendar_routes.py","lineNumber":1201,"sourceCode":"            events = q.order_by(CalendarEvent.dtstart).all()\n\n            # Expand recurring events into individual occurrences.\n            expanded = []\n            for e in events:\n                expanded.extend(_expand_rrule(e, start_dt, end_dt))\n\n            # Sort by occurrence start time for consistent frontend ordering.\n            truncated = any(e.get(\"truncated\") for e in expanded)\n            expanded.sort(key=lambda d: d[\"dtstart\"])\n            response: dict = {\"events\": expanded}\n            if truncated:\n                response[\"truncated\"] = True\n            return response\n        except HTTPException:\n            raise\n        except Exception as e:\n            logger.error(\"Failed to list events: %s\", e)\n            raise HTTPException(500, \"Failed to list events\")\n        finally:\n            db.close()\n\n    @router.post(\"/events\")\n    async def create_event(request: Request, data: EventCreate):\n        owner = _require_user(request)\n        _reserve_calendar_uploads(request, data.color, data.description, data.location)\n        db = SessionLocal()\n        try:\n            cal = None\n            if data.calendar_href:\n                cal = db.query(CalendarCal).filter(CalendarCal.id == data.calendar_href).first()\n                # Reject calendars that aren't owned by the caller. The\n                # previous `if cal and cal.owner and ...` check silently\n                # passed null-owner (legacy) rows, letting any authenticated\n                # user write events into them. Same null-owner gate as\n                # `_get_or_404_calendar`.\n                if cal and (cal.owner is None or cal.owner != owner):","sourceCodeStart":1183,"sourceCodeEnd":1219,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/calendar_routes.py#L1183-L1219","documentation":"Generic HTTP 500 from GET /events. After parsing the start/end range, the handler expands recurring events (rrule/EXDATE handling), sorts occurrences, and builds the response. Any exception in that pipeline — recurrence expansion errors, malformed stored rrule JSON, timezone data problems, or a DB failure — is logged as 'Failed to list events: <e>' and returned as this 500. Note malformed start/end parameters are deliberately caught earlier and return an empty list, so they do NOT cause this error.","triggerScenarios":"A stored event whose rrule or recurrence_exdates column holds malformed JSON or an impossible RRULE (e.g. COUNT=0, bogus FREQ) making the expansion engine throw; missing pytz/zoneinfo data for a stored timezone; database failure mid-query; a datetime overflow during expansion of a far-future recurring event.","commonSituations":"An event imported from a quirky ICS file left a nonstandard RRULE in the DB; a client wrote a DTSTART with an IANA timezone the runtime cannot resolve; DST edge in a timezone during expansion; an EXDATE that does not match any occurrence causing downstream code to fail.","solutions":["Inspect the server log ('Failed to list events: <e>') — it usually names the offending event uid or rrule.","Query CalendarEvent rows for this owner and check rrule/recurrence_exdates for malformed values; fix or delete the bad row(s).","Ensure timezone data is installed (tzdata package / system zoneinfo) if the error mentions a timezone lookup.","Narrow the start/end window — if the error only appears for wide ranges, a far-future expansion overflow is the likely culprit."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// sanity-check range before requesting\nconst s = new Date(start), e2 = new Date(end);\nif (isNaN(s) || isNaN(e2) || s > e2) throw new RangeError('bad range');\nawait api.get(`/events?start=${s.toISOString()}&end=${e2.toISOString()}`);","typeGuard":null,"tryCatchPattern":"try { await api.get('/events', {params: {start, end}}); }\ncatch (e) {\n  if (e.status === 500) {\n    // log contains the failing event/rrule; bisect the range to isolate it\n    const half = midpoint(start, end);\n    await Promise.all([fetchRange(start, half), fetchRange(half, end)]);\n  } else throw e;\n}","preventionTips":["Always send ISO 8601 start/end parameters; malformed ones silently return an empty list instead of an error, which hides data.","If 500s appear, narrow the date window to identify the corrupt recurring event, then fix or delete it.","Validate imported RRULEs before persisting them to keep malformed recurrence data out of the DB."],"tags":["http-500","recurrence","fastapi","database","rrule"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}