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
- Open the exact URL from the error (printed verbatim) with curl from the same host and inspect the status.
- If signed URLs expired: regenerate them, or pre-download the content and reference it from the workspace dir instead of a remote URL.
- If 5xx/429: retry the send after a backoff - the marker host is temporarily failing.
- 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
- Attach large or long-lived media from the workspace directory rather than remote URLs - no network dependency at send time.
- Use long-lived or non-expiring links for markers; refresh signed URLs before they lapse.
- Pre-flight marker URLs with a HEAD request when messages are queued.
- Keep marker hosts public - the fetch's SSRF guard refuses redirects to private hosts.
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
- matrix: marker URL {url} exceeded {MAX_MARKER_BYTES}-byte ca
- matrix: whoami request failed with HTTP {status}: {body}
- channel does not support room creation
- elicitation returned unknown choice const: {s}
- purge_namespace not supported by this memory backend
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/cebb60baca23c718.
Report an issue: GitHub.