fidius_host/error.rs
1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Error types for fidius-host plugin loading and calling.
16
17use fidius_core::PluginError;
18
19/// Errors that can occur when loading a plugin.
20#[derive(Debug, thiserror::Error)]
21pub enum LoadError {
22 #[error("library not found: {path}")]
23 LibraryNotFound { path: String },
24
25 #[error("symbol 'fidius_get_registry' not found in {path}")]
26 SymbolNotFound { path: String },
27
28 #[error("invalid magic bytes (expected FIDIUS\\0\\0)")]
29 InvalidMagic,
30
31 #[error("incompatible registry version: got {got}, expected {expected}")]
32 IncompatibleRegistryVersion { got: u32, expected: u32 },
33
34 #[error("incompatible ABI version: got {got}, expected {expected}")]
35 IncompatibleAbiVersion { got: u32, expected: u32 },
36
37 #[error("interface hash mismatch: got {got:#x}, expected {expected:#x}")]
38 InterfaceHashMismatch { got: u64, expected: u64 },
39
40 #[error("buffer strategy mismatch: plugin uses {got}, host expects {expected}")]
41 BufferStrategyMismatch {
42 got: fidius_core::descriptor::BufferStrategyKind,
43 expected: fidius_core::descriptor::BufferStrategyKind,
44 },
45
46 #[error("architecture mismatch: expected {expected}, got {got}")]
47 ArchitectureMismatch { expected: String, got: String },
48
49 #[error("unknown buffer strategy discriminant: {value}")]
50 UnknownBufferStrategy { value: u8 },
51
52 #[error("signature verification failed for {path}")]
53 SignatureInvalid { path: String },
54
55 #[error("signature required but no .sig file found for {path}")]
56 SignatureRequired { path: String },
57
58 #[error("plugin '{name}' not found")]
59 PluginNotFound { name: String },
60
61 #[error("libloading error: {0}")]
62 LibLoading(#[from] libloading::Error),
63
64 #[error("io error: {0}")]
65 Io(#[from] std::io::Error),
66
67 /// Python loader failed (only produced with the `python` feature on).
68 /// Wraps `fidius_python::PythonLoadError` as a string to keep the
69 /// fidius-host public error enum type-clean across feature gates.
70 #[error("python load failed: {0}")]
71 PythonLoad(String),
72
73 /// WASM component loader failed (only produced with the `wasm` feature on).
74 /// Wraps wasmtime/instantiation errors as a string to keep the public enum
75 /// type-clean across feature gates.
76 #[error("wasm load failed: {0}")]
77 WasmLoad(String),
78
79 /// Failed to serialize a plugin's config for construction (FIDIUS-A-0006 /
80 /// CI.2). Practically never happens for a well-formed config type.
81 #[error("config serialization failed: {0}")]
82 ConfigSerialization(String),
83}
84
85/// Errors that can occur when calling a plugin method.
86#[derive(Debug, thiserror::Error)]
87pub enum CallError {
88 #[error("serialization error: {0}")]
89 Serialization(String),
90
91 #[error("deserialization error: {0}")]
92 Deserialization(String),
93
94 #[error("plugin error: {0}")]
95 Plugin(PluginError),
96
97 #[error("plugin panicked: {0}")]
98 Panic(String),
99
100 #[error("buffer too small")]
101 BufferTooSmall,
102
103 /// Optional method is not implemented by this plugin — its capability bit is unset.
104 /// Returned when a method marked `#[optional]` is called on a plugin that chose not
105 /// to implement it. Not returned for out-of-range method indices; see `InvalidMethodIndex`.
106 #[error("method not implemented (capability bit {bit} not set)")]
107 NotImplemented { bit: u32 },
108
109 #[error("invalid method index {index} (plugin has {count} method(s))")]
110 InvalidMethodIndex { index: usize, count: u32 },
111
112 #[error("unknown FFI status code: {code}")]
113 UnknownStatus { code: i32 },
114
115 /// A method was dispatched through the wrong wire path — a `#[wire(raw)]`
116 /// method called via the typed path, or vice versa. Backend-agnostic: the
117 /// Python and (future) WASM executors both enforce the raw/typed split.
118 #[error(
119 "wire-mode mismatch on method '{method}': declared wire_raw={declared}, dispatcher used wire_raw={attempted}"
120 )]
121 WireModeMismatch {
122 method: String,
123 declared: bool,
124 attempted: bool,
125 },
126
127 /// A runtime-level fault originating inside an execution backend that is
128 /// *not* a plugin-raised [`PluginError`] — e.g. a future WASM trap
129 /// (unreachable, out-of-bounds) or an interpreter-level failure. Carries
130 /// the backend's runtime name and a message. Plugin-raised errors (Python
131 /// exceptions included) stay in [`CallError::Plugin`] so their structured
132 /// `code`/`message`/`details` (including tracebacks) round-trip.
133 #[error("{runtime} backend error: {message}")]
134 Backend { runtime: String, message: String },
135
136 /// A streaming backend produced bytes that did not decode as a valid
137 /// [`fidius_core::frame::Frame`] (bad tag, truncated, malformed payload).
138 /// Distinct from [`CallError::Deserialization`], which is about an item's
139 /// *contents*; this is about the framing around items. (FIDIUS-I-0026.)
140 #[error("malformed stream frame: {0}")]
141 MalformedFrame(String),
142
143 /// A stream ended without a terminal `END`/`ERROR` frame — the producer
144 /// went away mid-stream (e.g. a dropped backend task, a crashed
145 /// interpreter, a closed channel). (FIDIUS-I-0026.)
146 #[error("stream aborted before end-of-stream")]
147 StreamAborted,
148}
149
150/// Fold the Python backend's call error into the unified [`CallError`].
151///
152/// `fidius-python` deliberately does not depend on `fidius-host` (that would
153/// cycle — the host optionally depends on it), so the conversion lives here,
154/// behind the `python` feature, where both types are visible. Plugin-raised
155/// Python exceptions map to [`CallError::Plugin`] with the traceback preserved
156/// in `PluginError.details` (built by `fidius_python::pyerr_to_plugin_error`).
157#[cfg(feature = "python")]
158impl From<fidius_python::PythonCallError> for CallError {
159 fn from(e: fidius_python::PythonCallError) -> Self {
160 use fidius_python::PythonCallError as P;
161 match e {
162 P::InvalidMethodIndex { index, count } => CallError::InvalidMethodIndex {
163 index,
164 count: count as u32,
165 },
166 P::WireModeMismatch {
167 method,
168 declared,
169 attempted,
170 } => CallError::WireModeMismatch {
171 method: method.to_string(),
172 declared,
173 attempted,
174 },
175 P::InputDecode(msg) => CallError::Deserialization(msg),
176 P::OutputEncode(msg) => CallError::Serialization(msg),
177 P::Plugin(err) => CallError::Plugin(err),
178 }
179 }
180}