headroomlabs-ai/headroom · error · OfflineError

offline mode (HEADROOM_BINARIES_OFFLINE=1) but fetch require

Error message

offline mode (HEADROOM_BINARIES_OFFLINE=1) but fetch required: {url}

What it means

_download raises OfflineError when the HEADROOM_BINARIES_OFFLINE env var is truthy-set but the requested binary is not in the local cache, forcing a network fetch. This is an intentional air-gapped-mode guard: offline mode must never silently reach the network, so a cache miss becomes a hard error naming the URL that would have been fetched.

Source

Thrown at headroom/binaries.py:241


def _mirror_url(url: str) -> str:
    mirror = os.environ.get("HEADROOM_BINARIES_MIRROR")
    if not mirror:
        return url
    # Only substitute the github.com host so that paths remain intact.
    for prefix in ("https://github.com", "https://objects.githubusercontent.com"):
        if url.startswith(prefix):
            return mirror.rstrip("/") + url[len(prefix) :]
    return url


# ---------- Download + verify --------------------------------------------- #


def _download(url: str, dest: Path, *, progress: bool = True) -> None:
    if os.environ.get("HEADROOM_BINARIES_OFFLINE"):
        raise OfflineError(f"offline mode (HEADROOM_BINARIES_OFFLINE=1) but fetch required: {url}")
    if not _has_writable_existing_parent(dest.parent):
        raise OSError(f"binary cache directory parent is not writable: {dest.parent}")
    dest.parent.mkdir(parents=True, exist_ok=True)
    if not _is_writable_dir(dest.parent):
        raise OSError(f"binary cache directory is not writable: {dest.parent}")
    final_url = _mirror_url(url)
    req = urllib.request.Request(final_url, headers={"User-Agent": "headroom-binaries/1"})
    attempts = 3
    for attempt in range(1, attempts + 1):
        try:
            with urllib.request.urlopen(req, timeout=60) as resp:  # noqa: S310 (https)
                total = int(resp.headers.get("Content-Length") or 0)
                _stream_to(resp, dest, total, label=dest.name, show_progress=progress)
            return
        except urllib.error.URLError as e:
            dest.unlink(missing_ok=True)
            if attempt == attempts:
                raise BinaryFetchError(

View on GitHub (pinned to 322425c43b)

Solutions

  1. Warm the cache during image build with the same tool set, with the env var unset: RUN headroom ... (or the project's prefetch command), then set HEADROOM_BINARIES_OFFLINE=1 at runtime.
  2. If the network is actually available, unset the variable: HEADROOM_BINARIES_OFFLINE= (or remove it from the deployment spec).
  3. Point HEADROOM_BINARIES_MIRROR at an internal mirror and drop offline mode if direct GitHub access is the reason it was enabled.

Example fix

# before (Dockerfile)
ENV HEADROOM_BINARIES_OFFLINE=1
RUN headroom doctor  # OfflineError: cache empty

# after (Dockerfile)
RUN headroom doctor            # downloads and populates cache
ENV HEADROOM_BINARIES_OFFLINE=1
Defensive patterns

Strategy: validation

Validate before calling

import os

def offline_cache_complete(urls_or_tools: list[str]) -> bool:
    if not os.environ.get("HEADROOM_BINARIES_OFFLINE"):
        return True
    cache = binaries_cache_dir()  # wherever the runtime caches fetched tools
    return all((cache / t).exists() for t in urls_or_tools)

assert offline_cache_complete(["difft", "scc"]), "warm the cache before enabling offline mode"

Try / catch

try:
    ensure_binary(tool)
except OfflineError as e:
    raise SystemExit(f"offline mode set but {tool} not cached; run the prefetch step in the image build") from e

Prevention

When it happens

Trigger: HEADROOM_BINARIES_OFFLINE=1 (or any non-empty value) + first use of a tool on that machine/container, or after the cache directory was cleared.

Common situations: Air-gapped or CI environments where offline mode is set for reproducibility but the image was never warmed with a pre-fetch step; k8s emptyDir or ephemeral containers losing the cache between runs.

Related errors


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