zeroclaw-labs/zeroclaw · error

{method} by URL failed: {err}

Error message

{method} by URL failed: {err}

What it means

The send-media-by-URL path posts a JSON body (method such as sendPhoto/sendDocument with a URL instead of an upload) and bails with Telegram's response body on non-2xx. Telegram's servers fetch the URL themselves, so rejections usually point at the URL or its content rather than the chat.

Source

Thrown at crates/zeroclaw-channels/src/telegram.rs:3037

        if let Some(tid) = thread_id {
            body["message_thread_id"] = serde_json::Value::String(tid.to_string());
        }

        if let Some(cap) = caption {
            body["caption"] = serde_json::Value::String(cap.to_string());
        }

        let resp = self
            .http_client()
            .post(self.api_url(method))
            .json(&body)
            .send()
            .await?;

        if !resp.status().is_success() {
            let err = resp.text().await?;
            anyhow::bail!("{method} by URL failed: {err}");
        }

        ::zeroclaw_log::record!(
            INFO,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(
                ::serde_json::json!({"method": method, "chat_id": chat_id, "url": url})
            ),
            "sent to"
        );
        Ok(())
    }

    async fn send_attachment(
        &self,
        chat_id: &str,
        thread_id: Option<&str>,
        attachment: &TelegramAttachment,
    ) -> anyhow::Result<()> {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Fetch the URL yourself from a public network (curl with the same headers) — Telegram must be able to download it.
  2. Use the direct file URL with a Content-Type matching the method (image/jpeg for sendPhoto, etc.), not an HTML page.
  3. If the host is private, download the file locally and use the multipart path (send_photo/send_document) instead of the URL variant.
  4. Read the embedded `description` in the error body for the exact Bot API reason.
Defensive patterns

Strategy: fallback

Validate before calling

// prove the URL is publicly fetchable and the right content type before sending
let resp = reqwest::Client::new().head(url).send().await?;
anyhow::ensure!(resp.status().is_success(), "URL not fetchable: {}", resp.status());
anyhow::ensure!(resp.headers().get(CONTENT_TYPE).map_or(false, |v| v.to_str().unwrap_or("").starts_with("image/")), "URL is not an image");

Type guard

fn is_url_fetch_failure(err: &anyhow::Error) -> bool {
    err.to_string().contains("failed to get HTTP URL content")
}

Try / catch

if let Err(e) = channel.send_media_by_url(chat, url).await {
    if is_url_fetch_failure(&e) {
        // mirror the channel's own fallback: send the link as plain text
        return channel.send_text_chunks(&format!("{label}: {url}"), chat, thread).await;
    }
    return Err(e);
}

Prevention

When it happens

Trigger: 400 'failed to get HTTP URL content' when Telegram cannot fetch the URL (host unreachable from Telegram's network, wrong MIME type, redirect to an HTML page); malformed URL; 403 chat not found/blocked; content behind the URL exceeding the type's size limits.

Common situations: Links to localhost/intranet hosts Telegram cannot reach; expired signed URLs or hotlink protection; sending a web page URL where a direct file URL is required; content-type sniffing failing on unusual extensions.

Related errors


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