Skip to main content

camel_component_wasm/
error.rs

1//! WASM component error types.
2//!
3//! Provides structured error classification for WASM execution failures:
4//! - Timeout (epoch deadline expired)
5//! - Trap (guest panic, stack overflow, unreachable, etc.)
6//! - Out of memory (linear memory exceeded limit)
7//! - Unhealthy (consecutive failures, unable to re-instantiate)
8//!
9//! All variants carry context (plugin name, timeout value, etc.) for debugging
10//! and error handler integration (dead letter channel, retry policies).
11
12use camel_api::CamelError;
13
14/// Classification of a WASM trap reason.
15///
16/// Used to provide structured error information to error handlers
17/// instead of a flat string message.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum TrapReason {
20    /// Execution exceeded the configured timeout.
21    Timeout,
22    /// Linear memory exceeded the configured limit.
23    OutOfMemory,
24    /// `unreachable` instruction executed (guest panic).
25    Unreachable,
26    /// Call stack exceeded limit.
27    StackOverflow,
28    /// Other trap with description.
29    Other(String),
30}
31
32impl std::fmt::Display for TrapReason {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::Timeout => write!(f, "execution timeout"),
36            Self::OutOfMemory => write!(f, "out of memory"),
37            Self::Unreachable => write!(f, "unreachable instruction"),
38            Self::StackOverflow => write!(f, "stack overflow"),
39            Self::Other(msg) => write!(f, "{msg}"),
40        }
41    }
42}
43
44/// Errors that can occur during WASM plugin execution.
45#[derive(Debug, thiserror::Error)]
46pub enum WasmError {
47    #[error("WASM module not found: {0}")]
48    ModuleNotFound(String),
49
50    #[error("WASM compilation failed: {0}")]
51    CompilationFailed(String),
52
53    #[error("WASM instantiation failed: {0}")]
54    InstantiationFailed(String),
55
56    #[error("WASM guest panicked (trap): {0}")]
57    GuestPanic(String),
58
59    #[error("WASM type conversion failed: {0}")]
60    TypeConversion(String),
61
62    #[error("WASM I/O error: {0}")]
63    Io(String),
64
65    #[error("WASM configuration error: {0}")]
66    Config(String),
67
68    // ── Phase 4: Structured error variants ──────────────────────────────
69    #[error("WASM plugin '{plugin}' timed out after {timeout_secs}s")]
70    Timeout { plugin: String, timeout_secs: u64 },
71
72    #[error("WASM plugin '{plugin}' trapped: {reason}")]
73    Trap { plugin: String, reason: TrapReason },
74
75    #[error("WASM plugin '{plugin}' exceeded memory limit ({max_memory_bytes} bytes)")]
76    OutOfMemory {
77        plugin: String,
78        max_memory_bytes: u64,
79    },
80
81    #[error("WASM plugin '{plugin}' is unhealthy: {detail}")]
82    Unhealthy { plugin: String, detail: String },
83}
84
85impl WasmError {
86    /// Classify a wasmtime `Trap` into a `TrapReason`.
87    ///
88    /// Uses the trap type for well-known cases and falls back to
89    /// message matching for epoch-related traps.
90    ///
91    /// **Note:** `wasmtime::Trap` is `#[non_exhaustive]` — the `other` catch-all
92    /// arm is **required** and handles any future trap variants added by wasmtime.
93    pub fn classify_trap(trap: &wasmtime::Trap) -> TrapReason {
94        match trap {
95            wasmtime::Trap::StackOverflow => TrapReason::StackOverflow,
96            wasmtime::Trap::MemoryOutOfBounds => TrapReason::OutOfMemory,
97            wasmtime::Trap::UnreachableCodeReached => TrapReason::Unreachable,
98            wasmtime::Trap::Interrupt => TrapReason::Timeout,
99            // #[non_exhaustive] catch-all
100            other => {
101                let msg = other.to_string();
102                if msg.contains("epoch") {
103                    TrapReason::Timeout
104                } else {
105                    TrapReason::Other(msg)
106                }
107            }
108        }
109    }
110
111    /// Returns the plugin name associated with this error, if any.
112    pub fn plugin_name(&self) -> Option<&str> {
113        match self {
114            Self::Timeout { plugin, .. } => Some(plugin),
115            Self::Trap { plugin, .. } => Some(plugin),
116            Self::OutOfMemory { plugin, .. } => Some(plugin),
117            Self::Unhealthy { plugin, .. } => Some(plugin),
118            _ => None,
119        }
120    }
121}
122
123impl From<WasmError> for CamelError {
124    fn from(err: WasmError) -> Self {
125        match &err {
126            WasmError::GuestPanic(msg) => CamelError::ProcessorError(msg.clone()),
127            WasmError::TypeConversion(msg) => CamelError::TypeConversionFailed(msg.clone()),
128            WasmError::ModuleNotFound(msg) => CamelError::ComponentNotFound(msg.clone()),
129            WasmError::CompilationFailed(msg) => CamelError::EndpointCreationFailed(msg.clone()),
130            WasmError::InstantiationFailed(msg) => CamelError::EndpointCreationFailed(msg.clone()),
131            WasmError::Io(msg) => CamelError::Io(msg.clone()),
132            WasmError::Config(msg) => CamelError::Config(msg.clone()),
133            // ── Phase 4 structured variants ──
134            WasmError::Timeout {
135                plugin,
136                timeout_secs,
137            } => CamelError::ProcessorError(format!(
138                "WASM plugin '{}' timed out after {}s",
139                plugin, timeout_secs
140            )),
141            WasmError::Trap { plugin, reason } => {
142                CamelError::ProcessorError(format!("WASM plugin '{}' trapped: {}", plugin, reason))
143            }
144            WasmError::OutOfMemory {
145                plugin,
146                max_memory_bytes,
147            } => CamelError::ProcessorError(format!(
148                "WASM plugin '{}' exceeded memory limit ({} bytes)",
149                plugin, max_memory_bytes
150            )),
151            WasmError::Unhealthy { plugin, detail } => CamelError::ProcessorError(format!(
152                "WASM plugin '{}' is unhealthy: {}",
153                plugin, detail
154            )),
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_trap_reason_display() {
165        assert_eq!(TrapReason::Timeout.to_string(), "execution timeout");
166        assert_eq!(TrapReason::OutOfMemory.to_string(), "out of memory");
167        assert_eq!(
168            TrapReason::Unreachable.to_string(),
169            "unreachable instruction"
170        );
171        assert_eq!(TrapReason::StackOverflow.to_string(), "stack overflow");
172        assert_eq!(
173            TrapReason::Other("custom".to_string()).to_string(),
174            "custom"
175        );
176    }
177
178    #[test]
179    fn test_classify_trap_stack_overflow() {
180        let trap = wasmtime::Trap::StackOverflow;
181        assert!(matches!(
182            WasmError::classify_trap(&trap),
183            TrapReason::StackOverflow
184        ));
185    }
186
187    #[test]
188    fn test_classify_trap_memory_out_of_bounds() {
189        let trap = wasmtime::Trap::MemoryOutOfBounds;
190        assert!(matches!(
191            WasmError::classify_trap(&trap),
192            TrapReason::OutOfMemory
193        ));
194    }
195
196    #[test]
197    fn test_classify_trap_unreachable() {
198        let trap = wasmtime::Trap::UnreachableCodeReached;
199        assert!(matches!(
200            WasmError::classify_trap(&trap),
201            TrapReason::Unreachable
202        ));
203    }
204
205    #[test]
206    fn test_wasm_error_timeout_display() {
207        let err = WasmError::Timeout {
208            plugin: "my_plugin".to_string(),
209            timeout_secs: 30,
210        };
211        let msg = err.to_string();
212        assert!(msg.contains("my_plugin"));
213        assert!(msg.contains("30"));
214        assert!(msg.contains("timed out"));
215    }
216
217    #[test]
218    fn test_wasm_error_trap_display() {
219        let err = WasmError::Trap {
220            plugin: "my_plugin".to_string(),
221            reason: TrapReason::StackOverflow,
222        };
223        let msg = err.to_string();
224        assert!(msg.contains("my_plugin"));
225        assert!(msg.contains("stack overflow"));
226    }
227
228    #[test]
229    fn test_wasm_error_out_of_memory_display() {
230        let err = WasmError::OutOfMemory {
231            plugin: "my_plugin".to_string(),
232            max_memory_bytes: 52428800,
233        };
234        let msg = err.to_string();
235        assert!(msg.contains("my_plugin"));
236        assert!(msg.contains("52428800"));
237    }
238
239    #[test]
240    fn test_wasm_error_unhealthy_display() {
241        let err = WasmError::Unhealthy {
242            plugin: "my_plugin".to_string(),
243            detail: "consecutive failures".to_string(),
244        };
245        let msg = err.to_string();
246        assert!(msg.contains("my_plugin"));
247        assert!(msg.contains("consecutive failures"));
248    }
249
250    #[test]
251    fn test_wasm_error_to_camel_error_timeout() {
252        let err = WasmError::Timeout {
253            plugin: "p".to_string(),
254            timeout_secs: 10,
255        };
256        let camel: CamelError = err.into();
257        let msg = camel.to_string();
258        assert!(msg.contains("timed out"));
259        assert!(msg.contains("10"));
260    }
261
262    #[test]
263    fn test_wasm_error_to_camel_error_trap() {
264        let err = WasmError::Trap {
265            plugin: "p".to_string(),
266            reason: TrapReason::Unreachable,
267        };
268        let camel: CamelError = err.into();
269        let msg = camel.to_string();
270        assert!(msg.contains("unreachable"));
271    }
272
273    #[test]
274    fn test_wasm_error_to_camel_error_out_of_memory() {
275        let err = WasmError::OutOfMemory {
276            plugin: "p".to_string(),
277            max_memory_bytes: 1024,
278        };
279        let camel: CamelError = err.into();
280        let msg = camel.to_string();
281        assert!(msg.contains("memory"));
282    }
283
284    #[test]
285    fn test_wasm_error_to_camel_error_unhealthy() {
286        let err = WasmError::Unhealthy {
287            plugin: "p".to_string(),
288            detail: "broken".to_string(),
289        };
290        let camel: CamelError = err.into();
291        assert!(matches!(camel, CamelError::ProcessorError(_)));
292    }
293
294    #[test]
295    fn test_guest_panic_still_maps_to_processor_error() {
296        let err = WasmError::GuestPanic("boom".to_string());
297        let camel: CamelError = err.into();
298        assert!(matches!(camel, CamelError::ProcessorError(_)));
299    }
300
301    #[test]
302    fn test_plugin_name() {
303        let err = WasmError::Timeout {
304            plugin: "test".to_string(),
305            timeout_secs: 5,
306        };
307        assert_eq!(err.plugin_name(), Some("test"));
308
309        let err = WasmError::GuestPanic("msg".to_string());
310        assert_eq!(err.plugin_name(), None);
311    }
312}