cjpais/Handy · error · anyhow::Error

server returned HTTP {}

Error message

server returned HTTP {}

What it means

Generic non-success HTTP status on the download GET, after the specialized 416 and 200-to-Range handling. The actual status code is embedded in the message. The request reached the server, but the server refused it or failed — the reqwest error layer for connect/DNS failures is separate from this check.

Source

Thrown at src-tauri/src/managers/model/download.rs:259

        // just a broken server, which the generic status check below rejects.
        if resume_from > 0 && response.status() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE {
            if expected_size.is_some() || expected_sha256.is_none() {
                let _ = fs::remove_file(partial_path);
                return Err(anyhow::anyhow!(
                    "server object ends before the expected size (HTTP 416)"
                ));
            }
            Self::verify_file_with_events(model_id, partial_path, expected_sha256, emit).await?;
            return Ok(HttpDownloadOutcome::Completed);
        }
        // A 200 to a Range request means the server ignored it and is sending
        // the whole file; appending it to the partial would corrupt the model.
        if resume_from > 0 && response.status() == reqwest::StatusCode::OK {
            let _ = fs::remove_file(partial_path);
            resume_from = 0;
        }
        if !response.status().is_success() {
            return Err(anyhow::anyhow!(
                "server returned HTTP {}",
                response.status()
            ));
        }
        // On a 206, trust but verify the offset: a reply starting anywhere but
        // exactly our partial's end would silently corrupt the file on append.
        if resume_from > 0 && response.status() == reqwest::StatusCode::PARTIAL_CONTENT {
            let starts_at = response
                .headers()
                .get(reqwest::header::CONTENT_RANGE)
                .and_then(|v| v.to_str().ok())
                .and_then(content_range_start);
            if starts_at != Some(resume_from) {
                let _ = fs::remove_file(partial_path);
                return Err(anyhow::anyhow!(
                    "server returned Content-Range starting at {:?}, expected {}",
                    starts_at,
                    resume_from

View on GitHub (pinned to 98a4d80cce)

Solutions

  1. Read the embedded status: 429/5xx — wait and retry with backoff; 401/403 — authenticate or switch to an open source; 404 — the URL is wrong, update the catalog or the app
  2. Confirm the URL responds: curl -I <url>
  3. Switch the model source to the official HuggingFace endpoint
  4. Update Handy — catalog URL fixes ship with app releases
Defensive patterns

Strategy: retry

Validate before calling

async fn download_url_ok(url: &str) -> bool {
    match reqwest::Client::new().head(url).send().await {
        Ok(resp) => resp.status().is_success(),
        Err(_) => false,
    }
}

Try / catch

match downloader.download_http_resumable(...).await {
    Ok(outcome) => Ok(outcome),
    Err(e) if e.to_string().starts_with("server returned HTTP") => {
        let status = e.to_string();
        if status.contains("429") || status.contains("50") {
            tokio::time::sleep(backoff).await;   // transient: back off and retry
            downloader.download_http_resumable(...).await
        } else {
            Err(e)                               // 403/404: fix the URL/source instead
        }
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: 404/410 for dead or removed model URLs; 401/403 for gated HuggingFace repos or expired pre-signed URLs; 429 rate limiting; 5xx server outages; 451 geo-blocks.

Common situations: HF repo made private or deleted after the catalog shipped; mirror link rot; rate limits while re-downloading large models; CDN incidents.

Related errors


AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16). Data as JSON: /api/errors/30d5fba592996a4e. Report an issue: GitHub.