Skip to main content

furnace/
error.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::PathBuf;
4
5#[derive(Debug)]
6pub enum FurnaceError {
7    Model(ModelError),
8
9    Api(ApiError),
10
11    /// A nervous system that cannot be built, from [`Server::nerve`](crate::Server::nerve) or
12    /// a receptor or effector's constructor
13    Nerve(crate::nerve::BuildError),
14}
15
16impl FurnaceError {
17    /// Whether the request is at fault (its inputs do not fit the model), rather than the
18    /// server: a 400 rather than a 500
19    pub fn is_client_error(&self) -> bool {
20        matches!(
21            self,
22            Self::Model(
23                ModelError::InputValidation { .. }
24                    | ModelError::EmptyBatch
25                    | ModelError::BatchTooLarge { .. }
26                    | ModelError::InvalidDataType(_)
27                    | ModelError::InvalidInput(_)
28                    | ModelError::InvalidArgument { .. }
29            )
30        )
31    }
32}
33
34// Each variant is its inner error, message and source alike
35impl fmt::Display for FurnaceError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::Model(e) => fmt::Display::fmt(e, f),
39            Self::Api(e) => fmt::Display::fmt(e, f),
40            Self::Nerve(e) => fmt::Display::fmt(e, f),
41        }
42    }
43}
44
45impl std::error::Error for FurnaceError {
46    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
47        match self {
48            Self::Model(e) => e.source(),
49            Self::Api(e) => e.source(),
50            Self::Nerve(e) => e.source(),
51        }
52    }
53}
54
55impl From<ModelError> for FurnaceError {
56    fn from(e: ModelError) -> Self {
57        Self::Model(e)
58    }
59}
60
61impl From<ApiError> for FurnaceError {
62    fn from(e: ApiError) -> Self {
63        Self::Api(e)
64    }
65}
66
67impl From<crate::nerve::BuildError> for FurnaceError {
68    fn from(e: crate::nerve::BuildError) -> Self {
69        Self::Nerve(e)
70    }
71}
72
73#[derive(Debug)]
74pub enum ModelError {
75    LoadFailed {
76        path: PathBuf,
77        source: Box<dyn std::error::Error + Send + Sync>,
78    },
79
80    InvalidFormat(String),
81
82    FileNotFound(PathBuf),
83
84    InferenceFailed(String),
85
86    /// A simple model's input with the wrong number of values
87    InputValidation {
88        /// Items in the request
89        batch: usize,
90        /// One item's shape
91        item: Vec<usize>,
92        /// Values received
93        got: usize,
94    },
95
96    /// A request with no item
97    EmptyBatch,
98
99    /// A request with more items than one request may carry
100    BatchTooLarge {
101        size: usize,
102        max: usize,
103    },
104
105    InvalidDataType(String),
106
107    /// A named input tensor that does not fit the model (multi-tensor models)
108    InvalidInput(String),
109
110    InvalidArgument {
111        arg: String,
112        value: String,
113        reason: String,
114    },
115}
116
117impl fmt::Display for ModelError {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::LoadFailed { path, source } => {
121                write!(f, "Failed to load model from {}: {source}", path.display())
122            }
123            Self::InvalidFormat(m) => write!(f, "Invalid model format: {m}"),
124            Self::FileNotFound(path) => write!(f, "Model file not found: {}", path.display()),
125            Self::InferenceFailed(m) => write!(f, "Inference failed: {m}"),
126            Self::InputValidation { batch, item, got } => write!(
127                f,
128                "expected {} values (batch {batch} × {item:?}), got {got}",
129                batch * item.iter().product::<usize>()
130            ),
131            Self::EmptyBatch => {
132                f.write_str("a request needs at least one item, got an empty batch")
133            }
134            Self::BatchTooLarge { size, max } => write!(
135                f,
136                "batch size {size} exceeds the limit of {max} items per request"
137            ),
138            Self::InvalidDataType(m) => write!(f, "Invalid input data type: {m}"),
139            Self::InvalidInput(m) => write!(f, "Invalid input: {m}"),
140            Self::InvalidArgument { arg, value, reason } => {
141                write!(f, "Invalid argument '{arg}': {value} - {reason}")
142            }
143        }
144    }
145}
146
147impl std::error::Error for ModelError {
148    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
149        match self {
150            Self::LoadFailed { source, .. } => Some(&**source),
151            _ => None,
152        }
153    }
154}
155
156#[derive(Debug)]
157pub enum ApiError {
158    ServerStartup(String),
159}
160
161impl fmt::Display for ApiError {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::ServerStartup(m) => write!(f, "Server startup failed: {m}"),
165        }
166    }
167}
168
169impl std::error::Error for ApiError {}
170
171pub type Result<T> = std::result::Result<T, FurnaceError>;
172
173/// Structured error response for HTTP API
174#[derive(Debug, Serialize, Deserialize)]
175pub struct ErrorResponse {
176    pub error: String,
177    pub code: String,
178    pub details: Option<serde_json::Value>,
179    pub timestamp: String,
180}
181
182impl ErrorResponse {
183    pub fn new(error: String, code: String, details: Option<serde_json::Value>) -> Self {
184        Self {
185            error,
186            code,
187            details,
188            timestamp: crate::time::rfc3339_now(),
189        }
190    }
191}