headroomlabs-ai/headroom · warning · TimeoutError

native detect_content_type exceeded {timeout:.1f}s watchdog

Error message

native detect_content_type exceeded {timeout:.1f}s watchdog

What it means

Raised by the watchdog wrapper around the native (Rust/PyO3) detect_content_type in content_router.py: a daemon thread runs rust_detect(content) and if it is still alive after the timeout, the call is declared hung and TimeoutError is raised — you cannot interrupt a stuck native call from Python, so the thread is abandoned and the caller degrades instead of blocking forever. Any exception the thread did raise is re-raised verbatim afterwards.

Source

Thrown at headroom/transforms/content_router.py:863

    compression-executor worker — see #575's executor-saturation report).

    # ponytail: can't kill a GIL-released native call; the watchdog frees the
    # caller and the stuck daemon thread is left to die with the process. The
    # upgrade path is the Rust-side fix that makes first-call init non-blocking.
    """
    box: dict[str, Any] = {}

    def _run() -> None:
        try:
            box["result"] = rust_detect(content)
        except BaseException as exc:  # noqa: BLE001 — relayed to the caller's degrade path
            box["error"] = exc

    worker = threading.Thread(target=_run, name="headroom-detect-watchdog", daemon=True)
    worker.start()
    worker.join(timeout)
    if worker.is_alive():
        raise TimeoutError(f"native detect_content_type exceeded {timeout:.1f}s watchdog")
    if "error" in box:
        raise box["error"]
    return box["result"]


# Coding agents commonly wrap each tool result in an envelope such as
# ``<returncode>0</returncode>\n<output>...</output>`` (or <stdout>/<stderr>/
# <tool_result>). Those wrapper tags make the native detector read the whole
# payload as markup (HTML/XML) even though the inner content is source code, a
# grep result, or a log. That misroutes to the HTML article-extractor, which
# blanks or corrupts code (dropping identifiers and route converters). Detect on
# the inner payload so the real content type wins; compression still runs on the
# original content.
_DETECTION_ENVELOPE_RE = re.compile(
    r"\A\s*(?:<returncode>\s*-?\d+\s*</returncode>\s*)?"
    r"<(?P<tag>output|stdout|stderr|tool_result|result)>\n?"
    r"(?P<body>.*?)"
    r"\n?</(?P=tag)>\s*\Z",

View on GitHub (pinned to 322425c43b)

Solutions

  1. Catch TimeoutError and fall back to the pure-Python detector or treat content as plain text (this is the designed degrade path).
  2. If it reproduces, minimize the input and report it — a detector exceeding the watchdog indicates a native bug worth filing.
  3. Raise the watchdog timeout only if profiling shows legitimate large inputs, not a hang.

Example fix

# before
ctype = detect_content_type_wrapped(content)  # may hang -> TimeoutError

# after
try:
    ctype = detect_content_type_wrapped(content)
except TimeoutError:
    ctype = "text/plain"  # designed degrade path
Defensive patterns

Strategy: fallback

Try / catch

try:
    ctype = detect_wrapped(content)
except TimeoutError:
    logger.warning("native detect timed out; degrading to text/plain")
    ctype = "text/plain"

Prevention

When it happens

Trigger: Calling the native content detection path with input that makes the Rust detector loop or take longer than the watchdog timeout — e.g. pathological regex-ish payloads, huge markup-nested tool envelopes, or adversarial content.

Common situations: Compressing very large or deeply nested tool outputs through the content router; a Rust detector regression on a new input shape; slow/loaded machines pushing a normally-fast parse past the timeout.

Understand the failure class

Related errors


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