ducaale/xh · error

Could not create file after unreasonable number of attempts

Error message

Could not create file after unreasonable number of attempts

What it means

download_file panics via panic! after the retry loop in open_new_file exhausts all candidate filename attempts (N, N-1, N-2, ...) without being able to create an unused file. It is a deliberate internal-invariant violation rather than a recoverable Result error, meaning the filesystem appears to reject every candidate name.

Solutions

  1. Check write permissions on the target download directory before running
  2. Free disk space and verify the filesystem is not read-only
  3. Catch the panic at the download_file boundary (catch_unwind) or refactor open_new_file to return a typed error
  4. Choose a different, writable output directory explicitly

Example fix

// before
let (path, file) = download_file(...).unwrap(); // panics
// after
let result = std::panic::catch_unwind(|| download_file(...));
match result {
    Ok(Ok((path, file))) => { /* download */ },
    _ => eprintln!("could not create download file; check directory permissions"),
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling download_file
let dir = target_dir.as_path();
if !dir.is_dir() { anyhow::bail!("not a directory: {}", dir.display()); }
let probe = dir.join(".write_probe");
std::fs::File::create(&probe).and_then(|_| std::fs::remove_file(&probe))
    .context("download directory is not writable");

Type guard

fn is_writable_dir(p: &Path) -> bool {
    p.is_dir() && std::fs::metadata(p).map(|m| !m.permissions().readonly()).unwrap_or(false)
}

Try / catch

let result = std::panic::catch_unwind(AssertUnwindSafe(|| download_file(&url, &out_dir)));
match result {
    Ok(Ok((path, file))) => { /* use file */ }
    Ok(Err(e)) => eprintln!("download failed: {e}"),
    Err(_) => eprintln!("could not create file after unreasonable number of attempts"),
}

Prevention

When it happens

Trigger: Downloading to a directory where the base name and every enumerated fallback candidate cannot be opened new (try_open_new returns None for all iterations), e.g. an unwritable directory or extreme filesystem contention.

Common situations: Download directory lacks write permission; candidate names collide with files that can't be opened due to permissions; a pathological FS where open with O_CREAT|O_EXCL fails for all candidates.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at src/download.rs:105

            Ok(file) => Ok(Some(file)),
            Err(err) if err.kind() == ErrorKind::AlreadyExists => Ok(None),
            Err(err) => Err(err),
        }
    }
    if let Some(file) = try_open_new(&file_name)? {
        return Ok((file_name, file));
    }
    for suffix in 1..u32::MAX {
        let candidate = {
            let mut candidate = file_name.clone().into_os_string();
            candidate.push(format!("-{suffix}"));
            PathBuf::from(candidate)
        };
        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()

View on GitHub (pinned to 2404aceecc)