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

Raised when a Range resume request gets HTTP 416 (Range Not Satisfiable) and the server's object provably ends before the expected size — with a catalog-pinned expected_size, or with no hash that could vouch for the partial. The server is saying 'my file ends at or before where your partial begins', so the partial can never be completed; it is deleted and a clean restart is the only safe path.

Solutions

  1. Retry — the partial is already deleted, so the next attempt downloads fresh from byte 0.
  2. If it repeats immediately, the catalog metadata (expected size) disagrees with the server's actual object — switch to the primary URL or report the catalog entry.
  3. Clear the model's partial files manually only if the app cannot (unusual; deletion already happened here).
  4. Confirm no local clock/state weirdness by deleting the model entry entirely and re-adding it from the current catalog.
Defensive patterns

Strategy: retry

Try / catch

match manager.download_model(model_id, None).await {
    Ok(_) => Ok(()),
    Err(e) if e.to_string().contains("HTTP 416") => {
        // partial deleted automatically; a fresh attempt re-downloads from zero
        manager.download_model(model_id, None).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Resuming from a large partial against a server whose object shrank or was replaced by a smaller one; a catalog entry whose expected_size is larger than the file the mirror actually hosts; stale partials after the upstream re-published a smaller model.

Common situations: Mirror divergence: primary host re-uploaded a smaller/repacked artifact while the catalog kept the old size; interrupted downloads resumed days later after the file changed upstream.

Understand the failure class

Related errors


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

Appendix: 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 c89b7bf389)