cjpais/Handy · error · anyhow::Error

SHA256 task panicked

Error message

SHA256 task panicked: {}

What it means

Raised when the tokio::task::spawn_blocking job that runs verify_sha256 comes back with a JoinError, meaning the blocking task panicked or was aborted instead of returning a result. Because the hasher code itself mostly returns Err on I/O problems, a panic usually indicates the file was removed underneath the task, or the runtime is shutting down and cancelled the blocking task. The double '?' maps the JoinError into this anyhow message.

Solutions

  1. Retry the download — verification runs again at the end.
  2. Avoid deleting or touching the models directory while a download/verification is in flight.
  3. If it happens on every quit-during-download, let the download finish or cancel through the app's own cancel flow rather than killing the process.
  4. Check logs for the panic backtrace (RUST_BACKTRACE=1) if it occurs without cancellation — that indicates a real bug worth reporting.
Defensive patterns

Strategy: retry

Try / catch

match manager.download_model(model_id, None).await {
    Ok(_) => Ok(()),
    Err(e) if e.to_string().contains("SHA256 task panicked") => {
        // blocking task died (often shutdown race); a full retry is safe
        manager.download_model(model_id, None).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: verify_file_with_events races: the task panics while hashing (e.g., path metadata invalidated), or the tokio runtime is being torn down (app quit) and the JoinHandle is cancelled, yielding JoinError::cancelled.

Common situations: User cancels/quits during post-download verification; a second instance or cleanup process deletes the partial; bugs where the download state machine drops the future mid-verification.

Related errors


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

Appendix: source

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

        Ok(format!("{:x}", hasher.finalize()))
    }

    /// Emit verification events around a blocking sha256 check of `path`.
    /// On mismatch `verify_sha256` deletes the file, so the next attempt (or
    /// next source) starts clean. A `None` hash skips checking (custom models).
    async fn verify_file_with_events(
        model_id: &str,
        path: &Path,
        expected_sha256: Option<&str>,
        emit: &(dyn Fn(HttpDownloadEvent<'_>) + Send + Sync),
    ) -> Result<()> {
        emit(HttpDownloadEvent::VerificationStarted);
        let path = path.to_path_buf();
        let expected = expected_sha256.map(str::to_string);
        let id = model_id.to_string();
        tokio::task::spawn_blocking(move || Self::verify_sha256(&path, expected.as_deref(), &id))
            .await
            .map_err(|e| anyhow::anyhow!("SHA256 task panicked: {}", e))??;
        emit(HttpDownloadEvent::VerificationCompleted);
        Ok(())
    }

    /// [`Self::download_http_resumable_with_events`] wired to the Tauri event
    /// bus — the production entry point.
    pub(super) async fn download_http_resumable(
        &self,
        model_id: &str,
        url: &str,
        partial_path: &Path,
        expected_size: Option<u64>,
        expected_sha256: Option<&str>,
        cancel_token: &CancellationToken,
    ) -> Result<HttpDownloadOutcome> {
        let app_handle = self.app_handle.clone();
        let id = model_id.to_string();
        Self::download_http_resumable_with_events(

View on GitHub (pinned to c89b7bf389)