zeroclaw-labs/zeroclaw · error · anyhow::Error

Failed to resolve host '{host}'

Error message

Failed to resolve host '{host}'

What it means

resolve_host_for_request performs the DNS lookup for the http_request tool via tokio::net::lookup_host and treats two outcomes as failures: the lookup returns an error (surfaced as "Failed to resolve host '{host}': {e}"), or it succeeds but yields zero addresses, which produces this exact message. The empty-success case is a defensive guard: the resolved IP list feeds SSRF validation, and an empty list would silently skip that check, so the request aborts instead. It almost always indicates a broken resolver environment rather than a bad hostname.

Source

Thrown at crates/zeroclaw-tools/src/http_request.rs:768

}

fn extract_port(url: &str) -> anyhow::Result<u16> {
    let parsed = reqwest::Url::parse(url)
        .map_err(|e| anyhow::Error::msg(format!("Invalid URL format: {e}")))?;

    parsed
        .port_or_known_default()
        .ok_or_else(|| anyhow::Error::msg("URL must include a valid port"))
}

async fn resolve_host_for_request(host: String, port: u16) -> anyhow::Result<Vec<SocketAddr>> {
    let addrs = tokio::net::lookup_host((host.as_str(), port))
        .await
        .map_err(|e| anyhow::Error::msg(format!("Failed to resolve host '{host}': {e}")))?
        .collect::<Vec<_>>();

    if addrs.is_empty() {
        anyhow::bail!("Failed to resolve host '{host}'");
    }

    Ok(addrs)
}

fn validate_resolved_ips_for_ssrf(
    host: &str,
    private_resolution_allowed: bool,
    ips: &[std::net::IpAddr],
    nat64_prefixes: &[domain_guard::Nat64Prefix],
) -> anyhow::Result<()> {
    if private_resolution_allowed {
        domain_guard::validate_resolved_ips_exclude_metadata(host, ips, nat64_prefixes)
    } else {
        domain_guard::validate_resolved_ips_are_public(host, ips, nat64_prefixes)
    }
}

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Run 'getent hosts <host>' and 'dig <host> @<resolver>' in the same environment (same container/pod) to confirm the resolver actually returns records.
  2. Inspect /etc/nsswitch.conf and /etc/resolv.conf; in minimal images ensure the 'dns' source is present and the nameserver is reachable.
  3. Retry after network/DNS setup settles — empty answers during resolver startup are transient.
  4. If relying on search domains, pass the fully qualified name instead of the short name.
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight the same lookup the tool performs:
async fn resolvable(host: &str, port: u16) -> bool {
    tokio::net::lookup_host((host, port)).await.map(|it| it.count() > 0).unwrap_or(false)
}

Try / catch

for attempt in 0..3 {
    match http_request_tool(url.clone()).await {
        Ok(res) => break Ok(res),
        Err(e) if e.to_string().starts_with("Failed to resolve host") && attempt < 2 => {
            tokio::time::sleep(Duration::from_millis(500 * (attempt + 1))).await;
        }
        Err(e) => break Err(e),
    }
}

Prevention

When it happens

Trigger: An http_request call to a host whose getaddrinfo-style lookup completes successfully but returns no addresses: minimal/musl containers with a stripped /etc/nsswitch.conf, resolvers that answer with zero A/AAAA records, transient states while systemd-resolved or a VPN DNS proxy is starting, or search-domain configurations where the queried name matches nothing.

Common situations: Distroless or Alpine-based containers without the dns module in nsswitch.conf, Docker/Kubernetes pods during network setup, hosts with an unreachable or flapping DNS server, and CI runners where /etc/resolv.conf points at a resolver that returns empty answers.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/0238ce8b64d27d2b. Report an issue: GitHub.