{"record":{"id":"5eca25803297fd94","repo":"headroomlabs-ai/headroom","slug":"unsupported-spreadsheet-format-suffix-support","errorCode":null,"errorMessage":"Unsupported spreadsheet format '{suffix}'. Supported: .xlsx, .xls","messagePattern":"Unsupported spreadsheet format '(.+?)'\\. Supported: \\.xlsx, \\.xls","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"headroom/transforms/spreadsheet_ingest.py","lineNumber":96,"sourceCode":"\n    Returns:\n        Mapping of sheet name to CSV-rendered text (empty sheets omitted).\n\n    Raises:\n        FileNotFoundError: If the path does not exist.\n        ValueError: If the file extension is unsupported.\n        ImportError: If the required parser dependency is not installed.\n    \"\"\"\n    p = Path(path)\n    if not p.exists():\n        raise FileNotFoundError(f\"Spreadsheet not found: {p}\")\n\n    suffix = p.suffix.lower()\n    if suffix == \".xlsx\":\n        return _load_xlsx(p)\n    if suffix == \".xls\":\n        return _load_xls(p)  # pragma: no cover - legacy .xls path, see _load_xls\n    raise ValueError(f\"Unsupported spreadsheet format '{suffix}'. Supported: .xlsx, .xls\")\n","sourceCodeStart":78,"sourceCodeEnd":97,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/transforms/spreadsheet_ingest.py#L78-L97","documentation":"The spreadsheet ingest function dispatches on the lowercased file suffix and only accepts `.xlsx` and `.xls`; any other suffix raises ValueError listing the supported formats. Note the check is suffix-based, not content-based — a real Excel file with an unusual extension, or an unsupported spreadsheet format entirely, both land here.","triggerScenarios":"Passing a `.csv`, `.xlsm`, `.ods`, `.numbers` file, or a path with no/odd extension (`.XLSX~`, temp-file suffixes) — anything whose `Path.suffix.lower()` is not exactly `.xlsx` or `.xls`.","commonSituations":"Users uploading LibreOffice/Google-Sheets exports in `.ods` or `.xlsx` variants like `.xlsm` (macro workbooks, which openpyxl read-only mode doesn't cover); temp files mid-write with mangled suffixes; routing logic that forwards any 'attachment' to the spreadsheet ingest.","solutions":["Convert the file to `.xlsx` before ingest (LibreOffice headless, pandas round-trip) for supported-but-unlisted formats like `.xlsm`/`.ods`","Route by content type upstream: send `.csv` to a CSV reader and only pass `.xlsx`/`.xls` to this API","Pre-validate the suffix in your intake layer and reject with a user-facing message listing accepted formats"],"exampleFix":"# before\nsheets = ingest_spreadsheet(Path(\"export.ods\"))\n\n# after\nfrom pathlib import Path\nSUPPORTED = {\".xlsx\", \".xls\"}\npath = Path(\"export.ods\")\nif path.suffix.lower() not in SUPPORTED:\n    subprocess.run([\"libreoffice\", \"--headless\", \"--convert-to\", \"xlsx\", path], check=True)\n    path = path.with_suffix(\".xlsx\")\nsheets = ingest_spreadsheet(path)","handlingStrategy":"validation","validationCode":"from pathlib import Path\nSUPPORTED_SUFFIXES = {\".xlsx\", \".xls\"}\n\ndef spreadsheet_or_none(path: str | Path) -> Path | None:\n    p = Path(path)\n    return p if p.suffix.lower() in SUPPORTED_SUFFIXES else None\n\nif (p := spreadsheet_or_none(upload)) is None:\n    return reject_upload(upload, accepted=list(SUPPORTED_SUFFIXES))\nsheets = ingest_spreadsheet(p)","typeGuard":"def is_supported_spreadsheet(path: str | Path) -> bool:\n    return Path(path).suffix.lower() in {\".xlsx\", \".xls\"}","tryCatchPattern":"try:\n    sheets = ingest_spreadsheet(path)\nexcept ValueError as e:\n    if \"Unsupported spreadsheet format\" in str(e):\n        sheets = ingest_csv(path)  # route to the right reader\n    else:\n        raise","preventionTips":["Validate suffix at the intake/API boundary with a clear user-facing error","Route .csv and other formats to their own readers before calling spreadsheet ingest","Convert .xlsm/.ods uploads to .xlsx at upload time"],"tags":["validation","file-format","spreadsheet","precondition"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}