headroomlabs-ai/headroom · warning

fixture {} declares transform={} but lives under {}

Error message

fixture {} declares transform={} but lives under {}

What it means

The second 404 gate in require_loopback (loopback_guard.py:216): the TCP peer is loopback but the Host header does not name a loopback host. This is the DNS-rebinding defense — an attacker's DNS name (e.g. attacker.com) resolves to 127.0.0.1 so the peer IP passes, but the browser-sent Host header betrays the non-local origin, and the request is 404'd.

Source

Thrown at crates/headroom-parity/src/lib.rs:100

/// Load every `*.json` fixture under `dir/<transform>/`.
pub fn load_fixtures_for(dir: &Path, transform: &str) -> Result<Vec<(PathBuf, Fixture)>> {
    let root = dir.join(transform);
    if !root.exists() {
        return Ok(Vec::new());
    }
    let mut out = Vec::new();
    for entry in fs::read_dir(&root).with_context(|| format!("reading {}", root.display()))? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        let bytes = fs::read(&path).with_context(|| format!("reading {}", path.display()))?;
        let fixture: Fixture = serde_json::from_slice(&bytes)
            .with_context(|| format!("parsing fixture {}", path.display()))?;
        if fixture.transform != transform {
            bail!(
                "fixture {} declares transform={} but lives under {}",
                path.display(),
                fixture.transform,
                transform
            );
        }
        out.push((path, fixture));
    }
    Ok(out)
}

/// Aggregate report of one comparator run.
#[derive(Debug, Default)]
pub struct Report {
    pub matched: usize,
    pub diffed: Vec<(PathBuf, String, String)>,
    pub skipped: Vec<(PathBuf, String)>,
}

View on GitHub (pinned to 322425c43b)

Solutions

  1. Use http://127.0.0.1:PORT or http://localhost:PORT directly
  2. Remove Host header overrides from your curl command
  3. If a name is required, use a name that resolves to a loopback literal and ensure the Host header itself is localhost or a 127.x address

Example fix

# before
curl -H 'Host: mybox.local' http://127.0.0.1:8080/debug/tasks

# after
curl http://127.0.0.1:8080/debug/tasks
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse
host = urlparse(url).hostname
assert host in ("127.0.0.1", "localhost") or host.startswith("127."), f"Host {host} will be 404'd by the loopback guard"

Prevention

When it happens

Trigger: Browsing to http://your-hostname:8080/debug/... where the hostname resolves to 127.0.0.1 but is not 'localhost'/an ip-literal; tools that set a custom Host header (e.g. curl -H 'Host: mybox.local'); a rebinding attack where a public domain flips to 127.0.0.1.

Common situations: Using a machine's hostname or a LAN DNS name that happens to resolve to loopback; curl with explicit Host overrides; legitimate dashboards accessed via a named vhost.

Related errors


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