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
- Open the file in a text editor and confirm it starts with BEGIN:VCALENDAR and ends with END:VCALENDAR.
- Re-export the calendar from the source application rather than hand-editing.
- Ensure the upload is the raw bytes (Content-Type multipart/form-data, file field 'file') and under ICS_MAX_BYTES (10 MB default).
- 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
- Accept only .ics files in the file picker and check the BEGIN:VCALENDAR header client-side.
- Keep files under ICS_MAX_BYTES (10 MB) — oversize uploads fail earlier with a size error.
- Re-export from the source calendar rather than editing ICS by hand.
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
- Failed to import ICS
- Failed to export ICS
- Current password is incorrect
- {key} must be an integer
- Password is required
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/cf8b8a4cfc71f8d3.
Report an issue: GitHub.