{"record":{"id":"2f17182628a3239d","repo":"odysseus-dev/odysseus","slug":"empty-uid","errorCode":null,"errorMessage":"empty uid","messagePattern":"empty uid","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"routes/calendar_routes.py","lineNumber":141,"sourceCode":"\n\ndef _safe_ics_filename(name: str) -> str:\n    \"\"\"Return a conservative .ics filename safe for Content-Disposition.\"\"\"\n    stem = name if isinstance(name, str) else \"\"\n    stem = re.sub(r\"[^A-Za-z0-9._-]\", \"_\", stem).strip(\"._-\")\n    if not stem:\n        stem = \"calendar\"\n    return f\"{stem[:128]}.ics\"\n\n\ndef _resolve_base_uid(uid: str) -> str:\n    \"\"\"Extract the base series UID from a compound occurrence UID.\n\n    Compound UIDs have the form ``{base_uid}::{date_suffix}``.\n    For plain UIDs (no ``::``), returns the UID unchanged.\n    \"\"\"\n    if not uid:\n        raise ValueError(\"empty uid\")\n    idx = uid.find(\"::\")\n    if idx == -1:\n        return uid       # plain UID — no suffix\n    base = uid[:idx]\n    if not base:\n        raise ValueError(\"malformed compound UID: missing base before ::\")\n    return base\n\n\nasync def _push_caldav_event_after_commit(owner: str, uid: str, action: str):\n    \"\"\"Best-effort CalDAV write-through. Local writes stay authoritative if\n    the remote server is unreachable; pending flags let /sync retry later.\"\"\"\n    try:\n        result = {\"ok\": True}\n        if action == \"create\":\n            from src.caldav_sync import push_event_create\n            result = await push_event_create(owner, uid)\n        elif action == \"update\":","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/calendar_routes.py#L123-L159","documentation":"ValueError('empty uid') raised by _resolve_base_uid when called with an empty (or None-after-truthiness) uid string. This helper splits compound recurrence uids of the form '{base}::{date}' and requires a non-empty input. It is an internal invariant violation, not a user-facing HTTP error, and typically surfaces as a 500 unless caught upstream.","triggerScenarios":"Calling _resolve_base_uid('') — e.g. an event payload with uid: '' or a missing uid field flowing into recurrence handling; programmatic callers passing None.","commonSituations":"Client submits an event without generating a uid; a sync/import path leaves uid empty; malformed CalDAV data with empty UID components.","solutions":["Generate a uid before saving events (uuid4 or the app's uid generator) so the helper never sees an empty string.","Validate uid presence at the route boundary and return 400 before reaching recurrence logic.","If seen in logs as 500, trace the caller passing the empty uid."],"exampleFix":"# before\nevent = {\"uid\": body.get(\"uid\", \"\"), \"title\": ...}\n\n# after\nimport uuid\nevent = {\"uid\": (body.get(\"uid\") or str(uuid.uuid4())).strip(), \"title\": ...}","handlingStrategy":"type-guard","validationCode":"if (!uid || !uid.trim()) return badRequest('uid is required');","typeGuard":"const hasUid = (e: {uid?: unknown}): e is {uid: string} =>\n  typeof e.uid === 'string' && e.uid.trim().length > 0;","tryCatchPattern":"try { base = _resolveBaseUid(uid); } catch (e) { /* ValueError: reject payload with 400, don't 500 */ }","preventionTips":["Always generate a uid (uuid4) when creating events.","Validate uid non-empty at the API boundary so this invariant error never reaches the helper.","Treat an uncaught ValueError here as a 500-class bug in the caller, not user input."],"tags":["validation","valueerror","calendar","recurrence","invariant"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}