headroomlabs-ai/headroom · error · HTTPException

Not Found

Error message

Not Found

What it means

Dashboard-adjacent protected routes deliberately return HTTP 404 'Not Found' when the caller is neither loopback nor a trusted dashboard client, mirroring the trust chain of /stats and /stats-lifetime. The 404 hides existence of settings endpoints from untrusted networks rather than revealing authorization state (issue #2466 context).

Source

Thrown at headroom/proxy/server.py:3415

        return JSONResponse(status_code=200, content=payload)

    # Loopback-only debug introspection (Unit 5). A remote IP gets 404 —
    # debug endpoints are invisible to external scanners.
    from headroom.proxy.debug_introspection import (
        collect_tasks as _collect_tasks,
    )
    from headroom.proxy.loopback_guard import require_loopback as _require_loopback
    from headroom.proxy.loopback_guard import require_same_origin as _require_same_origin

    def _require_loopback_or_trusted_dashboard_client(request: Request) -> None:
        """Allow loopback callers, or gateway-forwarded dashboard clients.

        Mirrors the trust chain already used by /stats and /stats-lifetime
        (see _request_can_view_dashboard_metadata) so the settings UI works
        the same way behind a reverse-proxy/gateway (issue #2466).
        """
        if not _request_can_view_dashboard_metadata(request, trusted_dashboard_client_cidrs):
            raise HTTPException(status_code=404)

    def _require_same_origin_or_trusted_dashboard_client(request: Request) -> None:
        """Same-origin CSRF guard for settings writes, trusted-dashboard aware.

        ``require_same_origin`` only accepts an ``Origin`` that itself names a
        loopback host, so a browser POST from a trusted-gateway dashboard
        client was rejected even though the paired GET routes allow that same
        caller (issue #2466). For non-loopback callers, accept an ``Origin``
        that matches this request's own Host header, provided the caller is
        already an IP-literal-Host, CIDR-trusted dashboard client. Loopback
        callers keep the stricter loopback-only origin check unchanged.
        """
        if not _request_is_loopback(request):
            origin = request.headers.get("origin")
            host_header = request.headers.get("host")
            if (
                origin
                and origin != "null"

View on GitHub (pinned to 322425c43b)

Solutions

  1. Call the endpoint from 127.0.0.1/::1 for local use.
  2. Configure the trusted dashboard client CIDR env/settings so the gateway-forwarded client IP is recognized.
  3. Verify X-Forwarded-For handling matches the documented proxy setup.

Example fix

# before
curl http://<lan-host>:<port>/v1/settings  # 404

# after
# run locally
curl http://127.0.0.1:<port>/v1/settings
# or configure trusted dashboard client CIDRs before proxying
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress, os
def client_trusted(ip: str) -> bool:
    addr = ipaddress.ip_address(ip)
    return addr.is_loopback or any(
        addr in ipaddress.ip_network(c)
        for c in os.environ.get("HEADROOM_TRUSTED_DASHBOARD_CLIENT_CIDRS", "").split(",")
        if c.strip()
    )

Try / catch

resp = await client.get(url)
if resp.status_code == 404:
    treat_as_unauthorized_or_missing(resp)  # do not blindly retry

Prevention

When it happens

Trigger: Hitting a settings/metadata route from a non-loopback address whose IP is not within trusted_dashboard_client_cidrs; missing or wrong X-Forwarded-For behind a reverse proxy so classification fails.

Common situations: Dashboard behind a reverse proxy without configuring trusted CIDRs; accessing the proxy from LAN; curl without expected forwarding headers.

Related errors


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