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

matrix: marker URL {url} returned HTTP status {status}

Error message

matrix: marker URL {url} returned HTTP status {status}

What it means

Marker delivery (content referenced by URL in outgoing messages) fetches the marker over HTTP via fetch_http, with an SSRF-guarded redirect policy (redirects to private hosts are blocked) and a 30-second timeout. A non-success HTTP status from the marker URL aborts the fetch with this message; the status code distinguishes a dead link (404), a permission problem (403/410), or a serving-side failure (429/5xx). This fetch is separate from Matrix API calls.

Source

Thrown at crates/zeroclaw-channels/src/matrix.rs:3306

            reqwest::Client::builder()
                .timeout(MARKER_HTTP_TIMEOUT)
                .redirect(redirect_policy)
                .user_agent("zeroclaw-matrix/1.0")
                .build()
                .expect("default reqwest client config never fails to build")
        })
    }

    pub(super) async fn fetch_http(url: reqwest::Url) -> Result<Vec<u8>> {
        let client = marker_http_client();
        let resp = client
            .get(url.clone())
            .send()
            .await
            .with_context(|| format!("fetch marker URL {url}"))?;
        let status = resp.status();
        if !status.is_success() {
            bail!("matrix: marker URL {url} returned HTTP status {status}");
        }
        let mut stream = resp.bytes_stream();
        let mut buf = Vec::new();
        while let Some(chunk) = stream.next().await {
            let chunk = chunk.with_context(|| format!("stream chunk from {url}"))?;
            if buf.len().saturating_add(chunk.len()) > MAX_MARKER_BYTES {
                bail!("matrix: marker URL {url} exceeded {MAX_MARKER_BYTES}-byte cap; refusing");
            }
            buf.extend_from_slice(&chunk);
        }
        Ok(buf)
    }

    pub(super) fn thread_anchor_from_message(
        outbox: &Outbox<'_>,
        message: &SendMessage,
    ) -> Option<OwnedEventId> {
        if outbox.reply_in_thread {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Open the exact URL from the error (printed verbatim) with curl from the same host and inspect the status.
  2. If signed URLs expired: regenerate them, or pre-download the content and reference it from the workspace dir instead of a remote URL.
  3. If 5xx/429: retry the send after a backoff - the marker host is temporarily failing.
  4. Verify the marker host is reachable from the agent's network (egress rules, DNS, proxy) and is public, since redirects to private hosts are refused.
Defensive patterns

Strategy: retry

Validate before calling

async fn marker_url_reachable(url: &str) -> bool {
    matches!(
        reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .build()
            .expect("default client config")
            .head(url)
            .send()
            .await,
        Ok(resp) if resp.status().is_success()
    )
}

Prevention

When it happens

Trigger: Delivering a message whose marker URL answers non-2xx: expired signed sharing link (403/410), deleted object (404), rate-limited or broken host (429/5xx), or a mistyped scheme/host in the marker URL.

Common situations: Signed S3/GCS URLs that expired before send; content moved or deleted at the origin; internal hosts unreachable from the agent's network; CDN hiccups; markdown pasted with a truncated URL.

Related errors


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