headroomlabs-ai/headroom · error · ValueError

Unsupported spreadsheet format '{suffix}'. Supported: .xlsx,

Error message

Unsupported spreadsheet format '{suffix}'. Supported: .xlsx, .xls

What it means

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.

Source

Thrown at headroom/transforms/spreadsheet_ingest.py:96

    Returns:
        Mapping of sheet name to CSV-rendered text (empty sheets omitted).

    Raises:
        FileNotFoundError: If the path does not exist.
        ValueError: If the file extension is unsupported.
        ImportError: If the required parser dependency is not installed.
    """
    p = Path(path)
    if not p.exists():
        raise FileNotFoundError(f"Spreadsheet not found: {p}")

    suffix = p.suffix.lower()
    if suffix == ".xlsx":
        return _load_xlsx(p)
    if suffix == ".xls":
        return _load_xls(p)  # pragma: no cover - legacy .xls path, see _load_xls
    raise ValueError(f"Unsupported spreadsheet format '{suffix}'. Supported: .xlsx, .xls")

View on GitHub (pinned to 322425c43b)

Solutions

  1. Convert the file to `.xlsx` before ingest (LibreOffice headless, pandas round-trip) for supported-but-unlisted formats like `.xlsm`/`.ods`
  2. Route by content type upstream: send `.csv` to a CSV reader and only pass `.xlsx`/`.xls` to this API
  3. Pre-validate the suffix in your intake layer and reject with a user-facing message listing accepted formats

Example fix

# before
sheets = ingest_spreadsheet(Path("export.ods"))

# after
from pathlib import Path
SUPPORTED = {".xlsx", ".xls"}
path = Path("export.ods")
if path.suffix.lower() not in SUPPORTED:
    subprocess.run(["libreoffice", "--headless", "--convert-to", "xlsx", path], check=True)
    path = path.with_suffix(".xlsx")
sheets = ingest_spreadsheet(path)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
SUPPORTED_SUFFIXES = {".xlsx", ".xls"}

def spreadsheet_or_none(path: str | Path) -> Path | None:
    p = Path(path)
    return p if p.suffix.lower() in SUPPORTED_SUFFIXES else None

if (p := spreadsheet_or_none(upload)) is None:
    return reject_upload(upload, accepted=list(SUPPORTED_SUFFIXES))
sheets = ingest_spreadsheet(p)

Type guard

def is_supported_spreadsheet(path: str | Path) -> bool:
    return Path(path).suffix.lower() in {".xlsx", ".xls"}

Try / catch

try:
    sheets = ingest_spreadsheet(path)
except ValueError as e:
    if "Unsupported spreadsheet format" in str(e):
        sheets = ingest_csv(path)  # route to the right reader
    else:
        raise

Prevention

When it happens

Trigger: 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`.

Common situations: 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.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/5eca25803297fd94. Report an issue: GitHub.