tonhowtf/omniget · error · anyhow::Error

HTTP (fatal) downloading segment

Error message

HTTP {} (fatal) downloading segment

What it means

Raised in download_segment_with_retry when the segment HTTP response status is a 4xx client error other than 429 (rate limit) and 408 (request timeout). These are treated as fatal: retrying the same URL will not help, so the error propagates immediately without further retries. The status code is embedded in the message for diagnosis.

Solutions

  1. Re-fetch the media playlist and use fresh segment URLs (especially for live streams where segments are pruned).
  2. For 401/403, refresh the auth token or re-sign the URL before downloading.
  3. Check whether the playlist is behind geo/IP restrictions or requires cookies/headers you are not sending.
  4. If segments vanish quickly on live streams, reduce the download lag behind the live edge.

Example fix

// before
if (400..500).contains(&code) && code != 429 && code != 408 {
    return Err(anyhow::anyhow!("HTTP {} (fatal) downloading segment", code));
}
// after
if (400..500).contains(&code) && code != 429 && code != 408 {
    if code == 403 || code == 401 {
        // refresh signed URL / token once, then retry instead of failing outright
        let fresh = refresh_segment_url(&seg_url).await?;
        return download_once(&fresh).await;
    }
    return Err(anyhow::anyhow!("HTTP {} (fatal) downloading segment {}", code, seg_url));
}
Defensive patterns

Strategy: validation

Validate before calling

// after resp, before treating as fatal
let code = resp.status().as_u16();
if (400..500).contains(&code) && code != 429 && code != 408 {
    if code == 401 || code == 403 {
        // refresh auth token / signed URL once before giving up
        refresh_credentials().await?;
    } else if code == 404 {
        // re-fetch playlist: live segments may have moved to a new URL
        refresh_playlist().await?;
    }
}

Try / catch

match result {
    Err(e) if e.to_string().contains("(fatal)") => {
        let code: u16 = /* parse from message */ 0;
        if code == 403 || code == 401 {
            // refresh auth and retry the segment once
        } else {
            return Err(e); // unrecoverable client error
        }
    }
    other => other?,
}

Prevention

When it happens

Trigger: Segment GET returns 404 (segment pruned from a live playlist), 403 (expired signed URL/token), or 401 (missing/invalid auth) — any 4xx except 429/408.

Common situations: Live stream VOD edge where old segments expire while downloading; signed CDN URLs (e.g., AWS CloudFront, Akamai) whose query-token expired mid-download; geo-blocked or auth-required manifests; HLS playlist referencing segments removed by the server.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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

Appendix: source

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

    cancel: &CancellationToken,
) -> anyhow::Result<Vec<u8>> {
    let mut last_err = None;
    for attempt in 0..max_retries {
        if cancel.is_cancelled() {
            anyhow::bail!("Download cancelled");
        }

        let result = tokio::time::timeout(SEGMENT_TIMEOUT, async {
            let resp = apply_referer_headers(client.get(url), referer)
                .header("User-Agent", user_agent)
                .send()
                .await?;

            let status = resp.status();
            if !status.is_success() {
                let code = status.as_u16();
                if (400..500).contains(&code) && code != 429 && code != 408 {
                    return Err(anyhow::anyhow!("HTTP {} (fatal) downloading segment", code));
                }
                return Err(anyhow::anyhow!("HTTP {} downloading segment", code));
            }

            resp.bytes()
                .await
                .map(|b| b.to_vec())
                .map_err(|e| anyhow::anyhow!(e))
        })
        .await;

        match result {
            Ok(Ok(data)) => return Ok(data),
            Ok(Err(e)) => {
                if e.to_string().contains("(fatal)") {
                    return Err(e);
                }
                last_err = Some(e);

View on GitHub (pinned to 8600b91f42)