{"record":{"id":"3567fed82ddd03fa","repo":"odysseus-dev/odysseus","slug":"malformed-compound-uid-missing-base-before","errorCode":null,"errorMessage":"malformed compound UID: missing base before ::","messagePattern":"malformed compound UID: missing base before ::","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"routes/calendar_routes.py","lineNumber":147,"sourceCode":"    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\":\n            from src.caldav_sync import push_event_update\n            result = await push_event_update(owner, uid)\n        elif action == \"delete\":\n            from src.caldav_sync import push_event_delete\n            result = await push_event_delete(owner, uid)\n        if result and not result.get(\"ok\") and not result.get(\"skipped\"):","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/calendar_routes.py#L129-L165","documentation":"ValueError raised by _resolve_base_uid when the uid contains '::' but the base segment before it is empty, e.g. '::20260814'. The compound format is '{base_uid}::{date_suffix}'; an empty base has no resolvable series, so the helper refuses it.","triggerScenarios":"Passing a malformed compound uid like '::2026-08-14' or ' ::x' (whitespace-only base) into recurrence resolution or an occurrence-edit endpoint.","commonSituations":"Client-side uid concatenation bug (base variable empty when building `${base}::${date}`); imported/corrupted CalDAV data with mangled UIDs; hand-crafted test payloads.","solutions":["Fix the uid construction site to guarantee a non-empty base before appending '::'.","Sanitize incoming uids at the API boundary: reject ones matching /^::/ with a 400.","Repair corrupted rows in the events table rather than working around the ValueError."],"exampleFix":"// before\nconst uid = `${base}::${date}`;   // base may be ''\n\n// after\nif (!base) throw new Error('base uid required');\nconst uid = `${base}::${date}`;","handlingStrategy":"type-guard","validationCode":"const okCompound = (uid) => !uid.includes('::') || uid.split('::')[0].length > 0;\nif (!okCompound(uid)) return badRequest('malformed uid');","typeGuard":"const isWellFormedUid = (u: string): boolean =>\n  u.length > 0 && !u.startsWith('::');","tryCatchPattern":"catch (e) { if (e instanceof ValueError && /malformed/.test(e.message)) { rejectPayload(); } }","preventionTips":["Never build compound uids without asserting the base segment is non-empty.","Reject uids matching /^::/ at request validation time.","Add a test for '${base}::${date}' construction with empty base."],"tags":["validation","valueerror","calendar","recurrence","uid-format"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}