ducaale/xh · error

Missing Content-Range header

Error message

Missing Content-Range header

What it means

download_file requires a Content-Range header when resuming a download. The server's response to the range request lacked the header entirely, so the client cannot determine the total length or confirm the resume position. Without it, resume bookkeeping is impossible.

Solutions

  1. Restart the download from scratch (offset 0) instead of resuming.
  2. Check the response status: resume only when the server replies 206 Partial Content.
  3. Verify the server supports Range requests (Accept-Ranges: bytes) before resuming.
  4. Bypass intermediaries (proxies/CDNs) that strip or mishandle range headers.

Example fix

// before
let total = total_for_content_range(header, resume)?;
// after
if response.status() == StatusCode::OK {
    return restart_download_from_zero();
}
let total = total_for_content_range(header, resume)?;
Defensive patterns

Strategy: validation

Validate before calling

let resp = client.get(url).header(header::RANGE, format!("bytes={}-", offset)).send()?;
if resp.status() != StatusCode::PARTIAL_CONTENT || !resp.headers().contains_key(header::CONTENT_RANGE) {
    // restart from scratch instead of resuming
}

Type guard

fn can_resume(resp: &Response) -> bool {
    resp.status() == StatusCode::PARTIAL_CONTENT
        && resp.headers().contains_key(header::CONTENT_RANGE)
}

Try / catch

match download_file(url, Some(offset)) {
    Err(e) if e.to_string().contains("Content-Range") => download_file(url, None),
    other => other,
}

Prevention

When it happens

Trigger: download_file called with Some(resume) and the HTTP response headers contain no CONTENT_RANGE entry (server answered without a Content-Range, e.g. with 200 instead of 206).

Common situations: Server ignores the Range header and returns 200 with the full body; resuming from servers/proxies that strip range support; remote file was deleted or replaced so no partial response was generated.

Related errors


AI-assisted analysis of ducaale/xh@2404aceecc (2026-09-13). Data as JSON: /api/errors/7a6d89b2f2a91769. Report an issue: GitHub.

Appendix: source

Thrown at src/download.rs:210

        dest_name = file_name;
        buffer = Box::new(open_opts.open(&dest_name)?);
    } else if test_pretend_term() || io::stdout().is_terminal() {
        let (new_name, handle) = open_new_file(get_file_name(&response, orig_url).into())?;
        dest_name = new_name;
        buffer = Box::new(handle);
    } else {
        dest_name = "<stdout>".into();
        buffer = Box::new(io::stdout());
    }

    let starting_length: u64;
    let total_length: Option<u64>;
    if let Some(resume) = resume {
        let header = response
            .headers()
            .get(CONTENT_RANGE)
            .ok_or_else(|| anyhow!("Missing Content-Range header"))?
            .to_str()
            .map_err(|_| anyhow!("Bad Content-Range header"))?;
        starting_length = resume;
        total_length = Some(total_for_content_range(header, starting_length)?);
    } else {
        starting_length = 0;
        total_length = get_content_length(response.headers());
    }

    let starting_time = Instant::now();

    let pb = if quiet {
        // Still counts the downloaded bytes, it just doesn't display anything.
        ProgressBar::hidden()
    } else if let Some(total_length) = total_length {
        eprintln!(
            "Downloading {} to {:?}",
            HumanBytes(total_length - starting_length),

View on GitHub (pinned to 2404aceecc)