zeroclaw-labs/zeroclaw · error

Telegram sendPhoto by URL failed: {err}

Error message

Telegram sendPhoto by URL failed: {err}

What it means

The URL variant of photo sending posts a JSON body (sendPhoto with a URL) and bails with Telegram's response body on non-2xx. Telegram's servers fetch and decode the image themselves, so rejections typically mean the URL did not yield a usable image.

Source

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

        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("sendPhoto"))
            .json(&body)
            .send()
            .await?;

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

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

    /// Send a video by URL (Telegram will download it)
    pub async fn send_video_by_url(
        &self,
        chat_id: &str,
        thread_id: Option<&str>,
        url: &str,
        caption: Option<&str>,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. curl -I the URL: expect 200 with image/jpeg or image/png content-type and no login redirect.
  2. Use the direct asset URL; for protected hosts, download and use send_photo multipart instead.
  3. Read the embedded `description` and match it against the URL's actual response.
  4. On 429, honor retry_after and resend.
Defensive patterns

Strategy: fallback

Validate before calling

let resp = reqwest::Client::new().head(url).send().await?;
anyhow::ensure!(resp.status().is_success(), "image URL not fetchable: {}", resp.status());
let ct = resp.headers().get(reqwest::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or("");
anyhow::ensure!(ct.starts_with("image/"), "URL content-type is {ct}, not an image");

Type guard

fn is_photo_url_failure(err: &anyhow::Error) -> bool {
    let s = err.to_string();
    s.contains("failed to get HTTP URL content") || s.contains("wrong type of the current file")
}

Try / catch

if let Err(e) = channel.send_photo_by_url(chat, url).await {
    if is_photo_url_failure(&e) {
        let bytes = reqwest::get(url).await?.bytes().await?;
        return channel.send_photo_bytes(chat, thread, bytes.to_vec(), "photo.jpg", None).await;
    }
    return Err(e);
}

Prevention

When it happens

Trigger: 400 'failed to get HTTP URL content' when the URL returns HTML, a redirect chain, or an unsupported format (SVG, AVIF); 400 'wrong type of the current file' for non-image content; 403 chat not found/blocked; 429 rate limit.

Common situations: Chart/image URLs that 302 to a login page; webp variants Telegram's fetcher rejects; expired signed URLs; page URLs pasted instead of the direct image asset.

Related errors


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