tonhowtf/omniget · error · anyhow::Error

Failed to fetch m3u8 after

Error message

Failed to fetch m3u8 after {} attempts

What it means

This is the terminal error of fetch_m3u8_with_retry: after max_retries attempts all failed, and if no specific last_err was recorded the generic message naming the attempt count is returned. It means the playlist could not be fetched despite retries with exponential-ish backoff and jitter.

Solutions

  1. Inspect the retried sub-errors (log last_err each attempt) to see the real cause (status vs transport).
  2. Increase max_retries and backoff for unreliable hosts.
  3. Re-obtain a fresh playlist URL before retrying the whole operation.
  4. Verify network/DNS/proxy connectivity to the stream host.

Example fix

// before
let playlist = fetch_m3u8_with_retry(client, url, referer, 3).await?;
// after
let playlist = fetch_m3u8_with_retry(client, url, referer, 5)
    .await
    .with_context(|| format!("fetching playlist {url}"))?;
Defensive patterns

Strategy: retry

Validate before calling

// Ensure retries are configured before calling
let max_retries = max_retries.max(3);

Try / catch

let text = fetch_m3u8_with_retry(client, url, referer, 5).await
    .with_context(|| format!("m3u8 fetch failed after retries: {url}"))?;

Prevention

When it happens

Trigger: All attempts to GET the m3u8 failed (network errors or non-success statuses) across max_retries; last_err was None only in degenerate cases (max_retries=0).

Common situations: Extended network outage; host hard-blocking the client; URL expired before the first attempt; overly small max_retries with a flaky CDN.

Related errors


AI-assisted analysis of tonhowtf/omniget@8600b91f42 (2026-09-12). Data as JSON: /api/errors/aba1670335fe1475. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/omniget-core/src/core/hls_downloader.rs:237

                        last_err =
                            Some(anyhow::anyhow!("HTTP {} fetching playlist", resp.status()));
                    } else {
                        match resp.text().await {
                            Ok(text) => return Ok(text),
                            Err(e) => last_err = Some(anyhow::anyhow!(e)),
                        }
                    }
                }
                Err(e) => last_err = Some(anyhow::anyhow!(e)),
            }
            if attempt < max_retries - 1 {
                let base = 500 * (attempt as u64 + 1);
                let jitter = rand::random::<u64>() % (base / 2 + 1);
                tokio::time::sleep(Duration::from_millis(base + jitter)).await;
            }
        }
        Err(last_err.unwrap_or_else(|| {
            anyhow::anyhow!("Failed to fetch m3u8 after {} attempts", max_retries)
        }))
    }

    #[allow(clippy::too_many_arguments)]
    async fn download_media_playlist(
        &self,
        m3u8_url: &str,
        output_path: &str,
        referer: &str,
        bytes_tx: Option<UnboundedSender<u64>>,
        cancel_token: CancellationToken,
        max_concurrent: u32,
        max_retries: u32,
    ) -> anyhow::Result<HlsDownloadResult> {
        // The media playlist is fetched a second time here, independently of
        // `fetch_m3u8_with_retry`. Skipping this branch would throw the
        // prefetched text away and hit the network anyway.
        let text = match self.prefetched_for(m3u8_url) {

View on GitHub (pinned to 8600b91f42)