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

Telegram file download failed: {}

Error message

Telegram file download failed: {}

What it means

After obtaining a file_path via getFile, the channel streams the bytes from the Bot API file URL and bails with the raw status when that download answers non-2xx (transport-level failures surface earlier as the 'Failed to download Telegram file' context). Rejections here are almost always expired or mismatched download credentials, since Telegram file links are short-lived and token-bound.

Source

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

        {
            return Ok(path.to_string());
        }

        Err(FileLookupError::classify(status, body.as_ref()))
    }

    /// Download a file from the Telegram CDN.
    async fn download_file(&self, file_path: &str) -> anyhow::Result<Vec<u8>> {
        let url = format!("{}/file/bot{}/{file_path}", self.api_base, self.bot_token);
        let resp = self
            .http_client()
            .get(&url)
            .send()
            .await
            .context("Failed to download Telegram file")?;

        if !resp.status().is_success() {
            anyhow::bail!("Telegram file download failed: {}", resp.status());
        }

        Ok(resp.bytes().await?.to_vec())
    }

    /// Extract (file_id, duration) from a voice or audio message.
    fn parse_voice_metadata(message: &serde_json::Value) -> Option<(String, u64)> {
        let voice = message.get("voice").or_else(|| message.get("audio"))?;
        let file_id = voice.get("file_id")?.as_str()?.to_string();
        let duration = voice
            .get("duration")
            .and_then(serde_json::Value::as_u64)
            .unwrap_or(0);
        Some((file_id, duration))
    }

    /// Extract attachment metadata from an incoming Telegram message (document or photo).
    /// Returns `None` for text-only, voice, and other unsupported message types.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Call getFile again with the same file_id right before downloading to mint a fresh file_path.
  2. Ensure the same bot token is used for both getFile and the /file/bot<token>/ URL.
  3. For files above 20MB, run a local telegram-bot-api-server and point api_base at it.
  4. Process voice/media messages promptly instead of draining a long backlog.

Example fix

// before
let path = get_file(&file_id).await?; // file_path possibly stale
download(path).await?;

// after
let path = get_file(&file_id).await?; // always re-request before download
download(&format!("{api_base}/file/bot{token}/{path}")).await?;
Defensive patterns

Strategy: retry

Validate before calling

// mint a fresh file_path right before downloading
let file_path = channel.get_file_path(&file_id).await?;
let url = format!("{api_base}/file/bot{token}/{file_path}");

Type guard

fn is_expired_file_link(err: &anyhow::Error) -> bool {
    let s = err.to_string();
    s.contains("Telegram file download failed: 404") || s.contains("file_path invalid")
}

Try / catch

for attempt in 0..2 {
    match channel.download_file(&file_id).await {
        Err(e) if attempt == 0 && is_expired_file_link(&e) => {
            continue; // re-request getFile and retry once
        }
        other => return other,
    }
}

Prevention

When it happens

Trigger: file_path expired — Bot API download links live roughly one hour, so processing queued/backfilled voice messages later fails; a different bot token used to build the download URL than the one that called getFile; file larger than the cloud Bot API 20MB download limit.

Common situations: Offline catch-up after downtime reprocessing old updates; two bots sharing one config so file_id and token disagree; large voice notes on the cloud API where getFile itself still succeeds.

Related errors


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