Skip to main content

hf_fetch_model/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Error types for hf-fetch-model.
4//!
5//! All fallible operations in this crate return [`FetchError`].
6//! [`FileFailure`] provides structured per-file error reporting.
7
8use std::path::PathBuf;
9
10/// Errors that can occur during model fetching.
11#[derive(Debug, thiserror::Error)]
12#[non_exhaustive]
13pub enum FetchError {
14    /// The `hf-hub` API returned an error.
15    #[error("hf-hub API error: {0}")]
16    Api(#[from] hf_hub::api::tokio::ApiError),
17
18    /// An I/O error occurred while accessing the local filesystem.
19    #[error("I/O error at {path}: {source}")]
20    Io {
21        /// The path that caused the error.
22        path: PathBuf,
23        /// The underlying I/O error.
24        source: std::io::Error,
25    },
26
27    /// The repository was not found or is inaccessible.
28    #[error("repository not found: {repo_id}")]
29    RepoNotFound {
30        /// The repository identifier that was not found.
31        repo_id: String,
32    },
33
34    /// Authentication failed (missing or invalid token).
35    ///
36    /// Reserved for future use. Currently, auth failures surface as
37    /// [`FetchError::Api`] because `hf-hub` does not distinguish them.
38    #[error("authentication failed: {reason}")]
39    Auth {
40        /// Description of the authentication failure.
41        reason: String,
42    },
43
44    /// An invalid glob pattern was provided for filtering.
45    #[error("invalid glob pattern: {pattern}: {reason}")]
46    InvalidPattern {
47        /// The glob pattern that failed to parse.
48        pattern: String,
49        /// Description of the parse error.
50        reason: String,
51    },
52
53    /// SHA256 checksum mismatch after download.
54    #[error("checksum mismatch for {filename}: expected {expected}, got {actual}")]
55    Checksum {
56        /// The filename that failed verification.
57        filename: String,
58        /// The expected SHA256 hex digest.
59        expected: String,
60        /// The actual SHA256 hex digest computed from the file.
61        actual: String,
62    },
63
64    /// A download operation timed out.
65    #[error("timeout downloading {filename} after {seconds}s")]
66    Timeout {
67        /// The filename that timed out.
68        filename: String,
69        /// The timeout duration in seconds.
70        seconds: u64,
71    },
72
73    /// One or more files failed to download.
74    ///
75    /// Contains the successful path and a list of per-file failures.
76    #[error("{} file(s) failed to download:{}", failures.len(), format_failures(failures))]
77    PartialDownload {
78        /// The snapshot directory (if any files succeeded).
79        path: Option<PathBuf>,
80        /// Per-file failure details.
81        failures: Vec<FileFailure>,
82    },
83
84    /// A chunked (multi-connection) download failed.
85    #[error("chunked download failed for {filename}: {reason}")]
86    ChunkedDownload {
87        /// The filename that failed.
88        filename: String,
89        /// Description of the failure.
90        reason: String,
91    },
92
93    /// An HTTP request to the `HuggingFace` API failed.
94    #[error("HTTP error: {0}")]
95    Http(String),
96
97    /// An invalid argument was provided.
98    #[error("{0}")]
99    InvalidArgument(String),
100
101    /// The repository exists but no files matched after filtering,
102    /// or the repository contains no files at all.
103    #[error("no files matched in repository {repo_id}")]
104    NoFilesMatched {
105        /// The repository identifier.
106        repo_id: String,
107    },
108
109    /// A `.safetensors` header is malformed or cannot be parsed.
110    #[error("safetensors header error for {filename}: {reason}")]
111    SafetensorsHeader {
112        /// The filename whose header failed to parse.
113        filename: String,
114        /// Description of the parse failure.
115        reason: String,
116    },
117
118    /// `inspect` was asked to read a file whose extension is not supported.
119    ///
120    /// Emitted before any parse attempt so users see a clear format mismatch
121    /// rather than a misleading header-parse error.
122    #[error("hf-fm inspect supports .safetensors only (got .{extension} for {filename})")]
123    UnsupportedInspectFormat {
124        /// The filename whose extension is unsupported.
125        filename: String,
126        /// The actual extension without the leading dot, or `unknown` if none.
127        extension: String,
128    },
129}
130
131/// A per-file download failure with structured context.
132#[derive(Debug, Clone)]
133pub struct FileFailure {
134    /// The filename that failed.
135    pub filename: String,
136    /// Human-readable description of the failure.
137    pub reason: String,
138    /// Whether this failure is likely to succeed on retry.
139    pub retryable: bool,
140}
141
142impl std::fmt::Display for FileFailure {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(
145            f,
146            "{}: {} (retryable: {})",
147            self.filename, self.reason, self.retryable
148        )
149    }
150}
151
152/// Formats a list of file failures for inclusion in the `PartialDownload` error message.
153fn format_failures(failures: &[FileFailure]) -> String {
154    let mut s = String::new();
155    for f in failures {
156        s.push_str("\n  - ");
157        s.push_str(f.filename.as_str());
158        s.push_str(": ");
159        s.push_str(f.reason.as_str());
160    }
161    s
162}