ducaale/xh · error

Can't parse Content-Range header, can't resume download

Error message

Can't parse Content-Range header, can't resume download

What it means

When resuming a download, the tool parses the server's Content-Range header with a regex expecting 'bytes <start>-<end>/<len|*>'. If the header doesn't match at all — wrong unit like 'items', missing 'bytes' prefix, malformed syntax — it aborts the resume because the byte offset can't be trusted.

Solutions

  1. Retry the resume; if it still fails, delete the partial file and restart the download from scratch
  2. Check the actual Content-Range header with -v/curl and confirm the unit is 'bytes' with valid syntax
  3. Bypass or fix intermediate proxies/CDNs that mangle range headers
Defensive patterns

Strategy: try-catch

Validate before calling

fn parseable_content_range(header: &str) -> bool {
    header.starts_with("bytes ") && header.contains('-') && header.contains('/')
}

Try / catch

match total_for_content_range(header, expected_start) {
    Ok(total) => resume_from(expected_start, total),
    Err(e) if e.to_string().contains("Can't parse Content-Range") => {
        eprintln!("Resume aborted: {e}; restarting download");
        restart_download();
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Server returns a non-bytes Content-Range unit, an unparsable or absent range portion (e.g. 'bytes */1234' is handled, but 'items 0-9/10' or 'bytes' alone is not), or a truncated/garbled header from a broken proxy.

Common situations: Resuming against CDNs or caches that rewrite headers, misconfigured reverse proxies stripping or mangling Content-Range, non-HTTP-standard servers on embedded devices.

Related errors


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

Appendix: source

Thrown at src/download.rs:119

        if let Some(file) = try_open_new(&candidate)? {
            return Ok((candidate, file));
        }
    }
    panic!("Could not create file after unreasonable number of attempts");
}

// https://github.com/httpie/httpie/blob/84c7327057/httpie/downloads.py#L44
// https://tools.ietf.org/html/rfc7233#section-4.2
fn total_for_content_range(header: &str, expected_start: u64) -> Result<u64> {
    let re_range = Regex::new(concat!(
        r"^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)",
        r"/(?:\*|(?P<complete_length>\d+))$"
    ))
    .unwrap();
    let caps = re_range
        .captures(header)
        // Could happen if header uses unit other than bytes
        .ok_or_else(|| anyhow!("Can't parse Content-Range header, can't resume download"))?;
    let first_byte_pos: u64 = caps
        .name("first_byte_pos")
        .unwrap()
        .as_str()
        .parse()
        .context("Can't parse Content-Range first_byte_pos")?;
    let last_byte_pos: u64 = caps
        .name("last_byte_pos")
        .unwrap()
        .as_str()
        .parse()
        .context("Can't parse Content-Range last_byte_pos")?;
    let complete_length: Option<u64> = caps
        .name("complete_length")
        .map(|num| {
            num.as_str()
                .parse()
                .context("Can't parse Content-Range complete_length")

View on GitHub (pinned to 2404aceecc)