zed-industries/zed · error · anyhow::Error

refusing to follow redirect to non-HTTP(S) URL {target}

Error message

refusing to follow redirect to non-HTTP(S) URL {target}

What it means

The fetch tool received a 3xx response whose Location header, resolved against the current URL, yields a scheme other than http/https (e.g. file://, ftp:, data:). As a defense against protocol smuggling and local-file exfiltration the tool refuses to follow the redirect and errors, naming the refused target URL.

Source

Thrown at crates/agent/src/tools/fetch_tool.rs:99

        let normalized = normalize_url(url);

        let mut response = http_client
            .get(&normalized, AsyncBody::default(), false)
            .await?;

        let status = response.status();
        if status.is_redirection() {
            let location = response
                .headers()
                .get("location")
                .context("redirect response is missing a Location header")?
                .to_str()
                .context("redirect response has an invalid Location header")?;
            let target = url::Url::parse(&normalized)
                .with_context(|| format!("could not parse URL {normalized:?}"))?
                .join(location)
                .with_context(|| format!("invalid redirect target {location:?}"))?;
            anyhow::ensure!(
                matches!(target.scheme(), "http" | "https"),
                "refusing to follow redirect to non-HTTP(S) URL {target}"
            );
            return Ok(FetchStep::Redirect(target.to_string()));
        }

        let mut body = Vec::new();
        response
            .body_mut()
            .read_to_end(&mut body)
            .await
            .context("error reading response body")?;

        if status.is_client_error() {
            let text = String::from_utf8_lossy(body.as_slice());
            bail!("status error {}, response: {text:?}", status.as_u16());
        }

View on GitHub (pinned to bc538def45)

Solutions

  1. If the resource is genuinely available over http(s), fetch that final URL directly and fix the server's redirect.
  2. If you own the server, emit absolute http(s) Location headers.
  3. Never relax the scheme check — it is a security boundary; only http and https are allowed.
  4. Treat occurrences on external sites as suspicious (possible exfiltration attempt) and stop following the chain.

Example fix

# before: server responds with
HTTP/3 301 Location: ftp://example.com/file

# after: server responds with
HTTP/3 301 Location: https://example.com/file
Defensive patterns

Strategy: validation

Validate before calling

// Resolve redirects yourself and verify every hop before fetching:
let mut current = url.to_string();
for _ in 0..MAX_REDIRECTS {
    let response = issue_request(&current).await?;
    if !response.status().is_redirection() { break; }
    let location = header_str(response.headers(), "location").unwrap_or("");
    let next = url::Url::parse(&current)?.join(location)?;
    ensure!(
        matches!(next.scheme(), "http" | "https"),
        "refusing to follow redirect to non-HTTP(S) URL {next}"
    );
    current = next.to_string();
}

Type guard

fn is_http_url(url: &str) -> bool {
    url::Url::parse(url)
        .map(|parsed| matches!(parsed.scheme(), "http" | "https"))
        .unwrap_or(false)
}

Try / catch

match fetch_tool_run(url).await {
    Err(err) if err.to_string().contains("non-HTTP(S) URL") => {
        // Redirect target refused by design — fetch an approved http(s) URL instead.
        report_refused_redirect(err.to_string());
    }
    other => other,
}

Prevention

When it happens

Trigger: A fetched URL returns a redirect with `Location: file:///...`, `ftp://...`, or any non-HTTP(S) URI; the parsed-and-joined target fails the http/https scheme check.

Common situations: Adversarial or compromised sites redirecting to local files; misconfigured servers emitting ftp:// links; test fixtures with exotic Location headers; model-supplied URLs that hop protocols.

Related errors


AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16). Data as JSON: /api/errors/7e6b3f460e79269d. Report an issue: GitHub.