cjpais/Handy · error · anyhow::Error

server object ends before the expected size (HTTP 416)

Error message

server object ends before the expected size (HTTP 416)

What it means

The resumer sent 'Range: bytes=<resume_from>-' and got HTTP 416 Range Not Satisfiable: the server's object ends at or before the partial's offset, so the partial cannot be completed by this object. When the catalog pins an expected size, or there is no sha256 to bless the partial, the downloader deletes the partial and fails so the next attempt starts clean (with a hash and no pinned size, it instead verifies the partial and completes).

Source

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

            r = tokio::time::timeout(DOWNLOAD_STALL_TIMEOUT, request.send()) => r
                .map_err(|_| anyhow::anyhow!(
                    "no response within {}s from {}",
                    DOWNLOAD_STALL_TIMEOUT.as_secs(), url
                ))??,
            _ = cancel_token.cancelled() => return Ok(HttpDownloadOutcome::Cancelled),
        };

        // 416 to our Range request means its start is at or past the object's
        // end. With a catalog size in hand that can only mean the server's
        // object is *smaller* than expected (a full-size partial never issues
        // a request — handled above), and with no hash there is no trusted
        // signal to bless the partial: both restart clean. Only a hash can
        // genuinely finish a partial here. Without a Range in flight a 416 is
        // 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()
            ));
        }

View on GitHub (pinned to 98a4d80cce)

Solutions

  1. Simply retry download_model — the partial was already removed, so the retry downloads from scratch
  2. If it recurs, compare the catalog's expected size with the server object: curl -sI <url> and check Content-Length
  3. Update the app/catalog so the pinned size matches the currently published file
Defensive patterns

Strategy: retry

Try / catch

match downloader.download_http_resumable(...).await {
    Ok(outcome) => Ok(outcome),
    Err(e) if e.to_string().contains("HTTP 416") => {
        // the downloader already removed the mismatched partial;
        // a plain retry performs a clean full download
        downloader.download_http_resumable(...).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Server replaced the file with a smaller one after the partial was created; the partial belongs to a different, larger build of the model; stale catalog size after an upstream re-publish.

Common situations: Mirrors re-publishing models between sessions; resuming a download days later across an upstream release; catalog metadata skew after an app update.

Understand the failure class

Related errors


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