ducaale/xh · error

Bad Content-Range header

Error message

Bad Content-Range header

What it means

The Content-Range header value retrieved during a resumed download is not valid UTF-8 (or otherwise not a valid header string), so it cannot be parsed. The client rejects it before attempting range parsing.

Solutions

  1. Restart the download without resume (offset 0).
  2. Inspect the raw response headers (curl -v) to identify the malformed value.
  3. Update/bypass the proxy or middleware corrupting headers.
  4. If the server is under your control, fix its Content-Range emission to valid ASCII per RFC 9110.
Defensive patterns

Strategy: fallback

Validate before calling

if let Some(v) = resp.headers().get(header::CONTENT_RANGE) {
    if v.to_str().is_err() {
        // treat as non-resumable, restart download
    }
}

Type guard

fn valid_content_range(resp: &Response) -> Option<&str> {
    resp.headers().get(header::CONTENT_RANGE)?.to_str().ok()
}

Try / catch

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

Prevention

When it happens

Trigger: download_file called with Some(resume) and response.headers().get(CONTENT_RANGE) returns a HeaderValue whose to_str() fails because the header bytes are not visible ASCII/UTF-8.

Common situations: Misbehaving or malicious server/proxy emitting non-ASCII bytes in headers; corrupted response through broken middleware; extremely unusual custom server implementations.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at src/download.rs:212

        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),
            dest_name
        );

View on GitHub (pinned to 2404aceecc)