Skip to main content

iii_sdk/
error.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5/// Errors returned by the III SDK.
6#[derive(Debug, Error, Clone, Serialize, JsonSchema)]
7pub enum Error {
8    #[error("iii is not connected")]
9    NotConnected,
10    #[error("invocation timed out")]
11    Timeout,
12    #[error("runtime error: {0}")]
13    Runtime(String),
14    #[error("remote error ({code}): {message}")]
15    Remote {
16        code: String,
17        message: String,
18        stacktrace: Option<String>,
19    },
20    #[error("handler error: {0}")]
21    Handler(String),
22    #[error("serialization error: {0}")]
23    Serde(String),
24    #[error("websocket error: {0}")]
25    WebSocket(String),
26    /// Fatal registration rejection: another live worker already holds this
27    /// worker name in the namespace, or the engine sent an unknown rejection
28    /// code. The SDK stops and does not reconnect. A
29    /// `FUNCTION_NAMESPACE_CONFLICT` is non-fatal, is logged, and does not
30    /// produce this error.
31    #[error(
32        "registration rejected ({code}) in namespace '{namespace}' by worker '{owner_worker_id}'"
33    )]
34    RegistrationRejected {
35        code: String,
36        namespace: String,
37        worker_name: Option<String>,
38        function_id: Option<String>,
39        owner_worker_id: String,
40    },
41}
42
43impl From<serde_json::Error> for Error {
44    fn from(err: serde_json::Error) -> Self {
45        Error::Serde(err.to_string())
46    }
47}
48
49impl From<String> for Error {
50    fn from(msg: String) -> Self {
51        Error::Handler(msg)
52    }
53}
54
55impl From<&str> for Error {
56    fn from(msg: &str) -> Self {
57        Error::Handler(msg.to_string())
58    }
59}
60
61impl From<tokio_tungstenite::tungstenite::Error> for Error {
62    fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
63        Error::WebSocket(err.to_string())
64    }
65}
66
67/// Structured invocation failure, mirroring the Node and Python `InvocationError`.
68///
69/// Produced from the [`Error::Remote`] variant via [`Error::invocation_error`].
70/// `function_id` is `None` from that accessor because the wire `Remote` payload
71/// does not carry it.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73pub struct InvocationError {
74    pub code: String,
75    pub message: String,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub function_id: Option<String>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub stacktrace: Option<String>,
80}
81
82impl std::fmt::Display for InvocationError {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}: {}", self.code, self.message)
85    }
86}
87
88impl std::error::Error for InvocationError {}
89
90impl Error {
91    /// If this is a remote invocation failure (`Error::Remote`), return its
92    /// structured form. Returns `None` for transport/serde/handler errors.
93    pub fn invocation_error(&self) -> Option<InvocationError> {
94        match self {
95            Error::Remote {
96                code,
97                message,
98                stacktrace,
99            } => Some(InvocationError {
100                code: code.clone(),
101                message: message.clone(),
102                function_id: None,
103                stacktrace: stacktrace.clone(),
104            }),
105            _ => None,
106        }
107    }
108}
109
110#[cfg(test)]
111mod invocation_error_tests {
112    use super::*;
113
114    #[test]
115    fn remote_error_yields_invocation_error() {
116        let err = Error::Remote {
117            code: "FORBIDDEN".into(),
118            message: "nope".into(),
119            stacktrace: Some("trace".into()),
120        };
121        let inv = err.invocation_error().expect("remote -> invocation");
122        assert_eq!(inv.code, "FORBIDDEN");
123        assert_eq!(inv.message, "nope");
124        assert_eq!(inv.stacktrace.as_deref(), Some("trace"));
125        assert_eq!(inv.to_string(), "FORBIDDEN: nope");
126    }
127
128    #[test]
129    fn non_remote_error_yields_none() {
130        assert!(Error::Timeout.invocation_error().is_none());
131    }
132}