zeroclaw-labs/zeroclaw · error

Generated image URL host must not end with a dot

Error message

Generated image URL host must not end with a dot

What it means

parse_public_https_url rejects generated image URLs whose host ends with a dot, i.e. a fully-qualified domain name written with its explicit DNS root label ('https://cdn.example.com./img.png'). The trailing dot is technically valid DNS syntax, but the strict check keeps the validated host spelling and the later DNS pin keyed on the normalized host consistent, so the tool refuses the root-label form instead of normalizing silently.

Source

Thrown at crates/zeroclaw-tools/src/image_gen.rs:42

fn parse_public_https_url(raw_url: &str) -> anyhow::Result<(reqwest::Url, String, u16)> {
    let raw_url = raw_url.trim();
    if raw_url.is_empty() || raw_url.chars().any(char::is_whitespace) {
        anyhow::bail!("Generated image URL must be a non-empty URL without whitespace");
    }

    let mut url = reqwest::Url::parse(raw_url).context("Invalid generated image URL")?;
    if url.scheme() != "https" {
        anyhow::bail!("Generated image URL must use HTTPS");
    }
    if !url.username().is_empty() || url.password().is_some() {
        anyhow::bail!("Generated image URL userinfo is not allowed");
    }

    let request_host = url
        .host_str()
        .ok_or_else(|| anyhow::Error::msg("Generated image URL must include a host"))?;
    if request_host.ends_with('.') {
        anyhow::bail!("Generated image URL host must not end with a dot");
    }
    let host = domain_guard::normalize_domain(request_host)
        .ok_or_else(|| anyhow::Error::msg("Generated image URL host is invalid"))?;
    let ip_literal = host.parse::<IpAddr>().ok();
    if domain_guard::is_private_or_local_host(&host) {
        anyhow::bail!("Generated image URL targets a local or non-global host");
    }
    if ip_literal.is_some_and(domain_guard::is_cloud_metadata_ip) {
        anyhow::bail!("Generated image URL targets a cloud metadata host");
    }

    // Request the host that was validated, not the raw spelling. Normalization
    // can change the host (a leading dot is stripped), and the DNS pin applied
    // by `generated_image_client_with_builder` is keyed on the normalized host.
    // Without this, `resolve_to_addrs` never matches the request host and
    // reqwest silently falls back to its own unvalidated lookup. IP-literal
    // hosts are left alone: they carry no DNS pin, and `set_host` rejects the
    // unbracketed IPv6 form that normalization produces.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Remove the trailing dot from the host in the URL producer (storage/CDN configuration or the code that builds the link).
  2. If you control the string before it reaches the tool, strip a single trailing '.' from the host part first.

Example fix

# before
https://cdn.example.com./files/img.png   # rejected

# after
https://cdn.example.com/files/img.png       # accepted
Defensive patterns

Strategy: validation

Validate before calling

fn host_without_root_dot(raw: &str) -> Option<String> {
    let u = reqwest::Url::parse(raw.trim()).ok()?;
    let h = u.host_str()?;
    (!h.ends_with('.')).then(|| h.to_string())
}

Type guard

fn is_root_label_free(u: &reqwest::Url) -> bool { u.host_str().is_some_and(|h| !h.ends_with('.')) }

Try / catch

if let Err(e) = validate_image_target(url, nat64).await {
    if e.to_string().contains("must not end with a dot") {
        // strip the trailing '.' at the URL producer and retry once
    }
}

Prevention

When it happens

Trigger: A fal.ai response or custom storage config yields a URL with a trailing dot in the host: URLs copied from DNS tooling (dig output), hostnames programmatically terminated with '.' to force FQDN resolution, or DNS libraries that return the absolute form.

Common situations: Custom domains for image storage configured with a trailing dot, URLs assembled by joining a DNS-resolved name with a scheme, and copy-paste from zone files or DNS debug output.

Related errors


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