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

audio download failed ({status}) for message {message_id}

Error message

audio download failed ({status}) for message {message_id}

What it means

LINE channel: download_audio_content GETs the message content (Messaging API content endpoint) using the channel access token and the message_id, and the response was non-2xx. The message includes the HTTP status and the specific message_id. Transport errors surface separately as reqwest::Error.

Source

Thrown at crates/zeroclaw-channels/src/line.rs:200

/// Download audio/voice message binary from the LINE Content API.
/// LINE stores message content at `https://api-data.line.me/v2/bot/message/{id}/content`.
/// Audio messages are typically M4A (`audio/x-m4a`).
async fn download_audio_content(
    client: &reqwest::Client,
    content_api_base_url: &str,
    channel_access_token: &str,
    message_id: &str,
) -> anyhow::Result<Vec<u8>> {
    let url = format!("{content_api_base_url}/v2/bot/message/{message_id}/content");
    let resp = client
        .get(&url)
        .bearer_auth(channel_access_token)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        anyhow::bail!("audio download failed ({status}) for message {message_id}");
    }

    let mut bytes = Vec::new();
    let mut stream = resp;
    while let Some(chunk) = stream.chunk().await? {
        bytes.extend_from_slice(&chunk);
        if bytes.len() as u64 > MAX_LINE_AUDIO_BYTES {
            anyhow::bail!(
                "audio exceeds {} byte limit for message {message_id}",
                MAX_LINE_AUDIO_BYTES
            );
        }
    }
    Ok(bytes)
}

fn build_webhook_router(state: Arc<LineState>) -> axum::Router {
    use axum::{Router, routing::post};

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Branch on status: 401 -> re-copy the current long-term channel access token into [channels.line.<alias>]; 404 -> content expired, skip transcription; 429/5xx -> retry with backoff.
  2. Reduce webhook-to-processing latency so audio is downloaded within the content retention window.
  3. Confirm one token per channel and that no other process uses a stale token for the same channel.
Defensive patterns

Strategy: fallback

Validate before calling

// Cheap credential probe before downloading content
async fn line_token_ok(client: &reqwest::Client, token: &str) -> bool {
    client.get("https://api.line.me/v2/bot/info")
        .bearer_auth(token).send().await
        .map(|r| r.status().is_success()).unwrap_or(false)
}

Type guard

fn is_line_audio_download_error(err: &anyhow::Error) -> bool {
    err.to_string().contains("audio download failed")
}

Try / catch

match line.download_audio_content(msg_id).await {
    Ok(bytes) => transcribe(bytes),
    Err(e) if is_line_audio_download_error(&e) => continue_without_transcription(msg).await,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 401 with a revoked or rotated channel access token, 404 when the message content has passed LINE's retention window or the message_id is invalid, 429/5xx under load or incidents.

Common situations: Webhook backlogs processed late so audio content expired; the channel access token was re-issued in the LINE console (old tokens are revoked immediately); wrong token configured for the alias.

Related errors


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