tonhowtf/omniget · warning

Range not satisfiable, restarting

Error message

Range not satisfiable, restarting

What it means

When resuming, download_single_stream sends a Range request for the existing .part bytes. If the server answers 416 RANGE_NOT_SATISFIABLE instead of 206 PARTIAL_CONTENT, the stored offset is no longer valid (file shrank, changed, or the server can't satisfy the range), so the .part file is deleted and this error is returned to signal the caller that the transfer will restart from scratch. The retry loop treats it as a retryable attempt.

Solutions

  1. Do nothing special beyond retrying — the library deletes the stale .part and the next attempt restarts from byte 0; this is usually self-healing.
  2. Manually delete the .part file (output + '.part') before re-calling download if you want a guaranteed clean restart in one attempt.
  3. Verify the remote file hasn't changed (size/ETag/URL) between the original attempt and the resume; if it has, download to a new output path.
  4. Ensure the same URL/mirror is used for resume — switching mirrors with different content makes the saved offset invalid.
  5. If a proxy strips Range support, bypass it or download without resume.

Example fix

// before
// stale .part from an older, larger version of the file
let file = downloader.download(&url, &out).await?; // -> 416, 'Range not satisfiable, restarting'
// after
let part = out.with_extension("part"); // match part_path_for() naming
let _ = std::fs::remove_file(&part);   // drop incompatible resume state up front
let file = downloader.download(&url, &out).await?; // clean full download
Defensive patterns

Strategy: retry

Validate before calling

// Rust: drop resume state whose size can't fit the current remote file
fn reset_stale_part(output: &std::path::Path, remote_len: u64) {
    let part = output.with_extension("part");
    if let Ok(m) = std::fs::metadata(&part) {
        if m.len() > remote_len {
            let _ = std::fs::remove_file(&part);
        }
    }
}

Try / catch

match downloader.download(&url, &out).await {
    Err(e) if e.to_string().contains("Range not satisfiable") => {
        // library already deleted the .part; one clean retry starts from scratch
        downloader.download(&url, &out).await
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling download on an output whose .part file exists and is non-empty, and the server responds HTTP 416 to the Range header — e.g. the .part is larger than the (new) file size, the remote file changed between sessions, or the server lost its ability to serve ranges.

Common situations: Resuming a download after the source file was replaced with a smaller version; a .part left over from a different URL written to the same output path; servers/proxies that previously supported ranges but now return 416 (config change, cache purge); clock/mirror switch changing content length mid-resume.

Related errors


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

Appendix: source

Thrown at src-tauri/omniget-core/src/core/direct_downloader.rs:502

    if existing_bytes > 0 {
        if let Some(total) = total_size {
            if existing_bytes >= total {
                return Ok(());
            }
        }
        request = request.header("Range", format!("bytes={}-", existing_bytes));
    }

    let response = request.send().await?;

    let mut offset = 0u64;
    if existing_bytes > 0 {
        if response.status() == reqwest::StatusCode::PARTIAL_CONTENT {
            offset = existing_bytes;
        } else if response.status() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE {
            let _ = std::fs::remove_file(part_path);
            return Err(anyhow!("Range not satisfiable, restarting"));
        } else if !response.status().is_success() {
            return Err(anyhow!("HTTP {} downloading {}", response.status(), url));
        }
    } else if !response.status().is_success() {
        return Err(anyhow!("HTTP {} downloading {}", response.status(), url));
    }

    if let Some(ct) = response.headers().get("content-type") {
        if let Ok(ct_str) = ct.to_str() {
            if ct_str.contains("text/html") {
                return Err(anyhow!(
                    "Server returned HTML instead of media — URL may have expired"
                ));
            }
        }
    }

    use std::io::Write;

View on GitHub (pinned to 8600b91f42)