odysseus-dev/odysseus · warning · HTTPException

Invalid ICS file: {e}

Error message

Invalid ICS file: {e}

What it means

Raised as HTTP 400 by POST /import when icalendar's Calendar.from_ical() throws while parsing the uploaded file. from_ical raises ValueError (or Index error) on content that is not a valid iCalendar stream: missing BEGIN:VCALENDAR/END:VCALENDAR wrapper, bare vEvent without a calendar, invalid line syntax, or decoding problems. The exception text is embedded in the 400 detail ('Invalid ICS file: <e>'). Note the read itself is size-limited beforehand (ICS_MAX_BYTES, default 10 MB), so oversize files fail earlier with a different message.

Source

Thrown at routes/calendar_routes.py:1423


    # Hard cap on ICS upload (ICS_MAX_BYTES, default 10 MB). Loading the whole
    # file into memory is unavoidable with python-icalendar, so an unbounded
    # upload would OOM.

    @router.post("/import")
    async def import_ics(request: Request, file: UploadFile = File(...), calendar_name: str = ""):
        """Import events from an .ics file (scoped to caller's account)."""
        from icalendar import Calendar as iCal

        owner = _require_user(request)
        db = SessionLocal()
        try:
            content = await read_upload_limited(file, ICS_MAX_BYTES, "ICS file")
            try:
                cal_data = iCal.from_ical(content)
            except Exception as e:
                raise HTTPException(400, f"Invalid ICS file: {e}")

            # Sanitize display name — length cap + strip control chars
            raw_name = calendar_name.strip() or (file.filename or "").replace(".ics", "").replace("_", " ").strip() or "Imported"
            cal_display = "".join(c for c in raw_name if c.isprintable())[:120] or "Imported"

            target_cal = db.query(CalendarCal).filter(
                CalendarCal.name == cal_display,
                CalendarCal.owner == owner,
            ).first()
            if not target_cal:
                target_cal = CalendarCal(
                    id=str(uuid.uuid4()),
                    owner=owner,
                    name=cal_display,
                    color="#7c4dff",
                    source="import",
                )
                db.add(target_cal)

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Open the file in a text editor and confirm it starts with BEGIN:VCALENDAR and ends with END:VCALENDAR.
  2. Re-export the calendar from the source application rather than hand-editing.
  3. Ensure the upload is the raw bytes (Content-Type multipart/form-data, file field 'file') and under ICS_MAX_BYTES (10 MB default).
  4. Check the 400 detail — icalendar's exception text usually points at the offending line.

Example fix

# before
# file contents: "Subject,Start
Lunch,1pm"  (a CSV renamed to .ics)
curl -F file=@events.csv /import

# after
# file contents: "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n...\r\nEND:VCALENDAR"
curl -F file=@calendar.ics /import
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeIcs(text) {
  return /BEGIN:VCALENDAR/.test(text.slice(0, 200)) && /END:VCALENDAR/.test(text.slice(-200));
}
const text = await file.text();
if (!looksLikeIcs(text)) throw new Error('Not a valid iCalendar file');
await upload(file); // parse errors avoided before the request

Try / catch

try { await api.post('/import', formData); }
catch (e) {
  if (e.status === 400) showError(e.detail); // detail embeds icalendar's parse reason
  else throw e;
}

Prevention

When it happens

Trigger: Uploading a .csv or .vcs file renamed to .ics; a text export missing the BEGIN:VCALENDAR header; a truncated file (upload interrupted); an HTML error page saved as .ics; non-UTF-8 bytes that break the parser.

Common situations: Users exporting from Google/Apple Calendar getting a partial download; corporate proxies serving an HTML login page instead of the file; hand-edited ICS with folded lines broken incorrectly; files over 10 MB hitting the separate size-limit error instead.

Related errors


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