Skip to main content

slim_session/
errors.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use slim_datapath::api::ProtoName;
5use slim_datapath::errors::{ErrorPayload, MessageContext};
6// Third-party crates
7use thiserror::Error;
8
9// Local crate
10use slim_auth::errors::AuthError;
11use slim_datapath::api::{ProtoMessage, ProtoSessionMessageType, ProtoSessionType};
12use slim_datapath::messages::utils::MessageError;
13use slim_mls::errors::MlsError;
14use tonic::Status;
15
16use crate::SessionMessage;
17use crate::subscription_manager::SubscriptionAckError;
18
19#[derive(Error, Debug)]
20pub enum SessionError {
21    // Transport and channel errors
22    #[error("SLIM channel closed")]
23    SlimChannelClosed,
24    #[error("error receiving message from SLIM")]
25    SlimReception(#[from] Status),
26
27    // Message processing and validation errors
28    #[error("message error")]
29    MessageError(#[from] MessageError),
30    #[error("invalid group update operation: {0}")]
31    InvalidGroupUpdateOp(i32),
32    #[error("missing removed participant in GroupRemove message")]
33    MissingRemovedParticipantInGroupRemove,
34    #[error("missing new participant in GroupAdd message")]
35    MissingNewParticipantInGroupAdd,
36    #[error("missing group name in JoinRequest message")]
37    MissingGroupNameInJoinRequest,
38    #[error("missing channel name for group session")]
39    MissingChannelName,
40    #[error("session type unknown: {0:?}")]
41    SessionTypeUnknown(ProtoSessionType),
42    #[error("session message type unexpected: {0:?}")]
43    SessionMessageInternalUnexpected(Box<SessionMessage>),
44    #[error("session message type unknown: {0:?}")]
45    SessionMessageTypeUnknown(ProtoSessionMessageType),
46    #[error("message type unexpected: {0:?}")]
47    MessageTypeUnexpected(Box<ProtoMessage>),
48    #[error("session message type unexpected: {0:?}")]
49    SessionMessageTypeUnexpected(ProtoSessionMessageType),
50    #[error("error getting the participants list")]
51    ParticipantsListQueryFailed,
52    #[error("malformed participant settings")]
53    MalformedParticipant,
54    #[error("missing participant settings")]
55    MissingParticipantSettings,
56    #[error("identity key is empty")]
57    SignatureKeyIsEmpty,
58    #[error("identity key collection failed with auth error: {0:?}")]
59    SignatureKeyCollectionFailedWithAuthErr(AuthError),
60    #[error("unexpected error")]
61    UnexpectedError { source: Box<SessionError> },
62
63    // Lookup and missing entities
64    #[error("session not found: {0}")]
65    SessionNotFound(u32),
66    #[error("subscription not found: {0}")]
67    SubscriptionNotFound(Box<ProtoName>),
68
69    // Session lifecycle and state
70    #[error("session builder: not all required fields set")]
71    SessionBuilderIncomplete,
72    #[error("message lost for session id: {0}")]
73    MessageLost(u32),
74    #[error("session closed")]
75    SessionClosed,
76    #[error("receive timeout waiting for message")]
77    ReceiveTimeout,
78    #[error("session id already used: {0}")]
79    SessionIdAlreadyUsed(u32),
80    #[error("invalid session id: {0}")]
81    InvalidSessionId(u32),
82
83    // Cryptography (MLS)
84    #[error("mls operation error")]
85    MlsOp(#[from] MlsError),
86
87    // Persistence / restore
88    #[error("persistence store error")]
89    Persistence(#[from] slim_persistence::PersistenceError),
90    #[error("failed to (de)serialize session persistence record")]
91    PersistenceSerde(#[from] serde_json::Error),
92    #[error("failed to decode persisted field: {0}")]
93    PersistenceDecode(String),
94    #[error("persisted session schema mismatch: expected {expected}, got {got}")]
95    PersistenceSchemaMismatch { expected: u32, got: u32 },
96
97    // Authorization and roles
98    #[error("auth error: {0}")]
99    Auth(#[from] AuthError),
100    #[error("duplicate control message replay: message_id={message_id}")]
101    ControlMessageReplay { message_id: u32 },
102
103    // Acknowledgements and routing
104    #[error("error receiving ack for message: {0}")]
105    AckReception(String),
106    #[error("subscription ack failed: {0}")]
107    SubscriptionAckFailed(#[source] SubscriptionAckError),
108    #[error("unknown destination: {0}")]
109    UnknownDestination(Box<ProtoName>),
110
111    // Session membership and permissions
112    #[error("participant not found in group: {0}")]
113    ParticipantNotFound(Box<ProtoName>),
114    #[error("participant already in group: {0}")]
115    ParticipantAlreadyInGroup(Box<ProtoName>),
116    #[error("cannot invite participant to point-to-point session")]
117    CannotInviteToP2P,
118    #[error("cannot remove participant from point-to-point session")]
119    CannotRemoveFromP2P,
120    #[error("cannot close a point-to-point session")]
121    CannotCloseP2P,
122    #[error("cannot rejoin a point-to-point session")]
123    CannotRejoinP2P,
124    #[error("only initiator can modify participants")]
125    NotInitiator,
126
127    // Routing and delivery failures
128    #[error("error sending session internal message to session controller")]
129    SessionControllerSendFailed,
130    #[error("error sending new session notification to app")]
131    NewSessionSendFailed,
132    #[error("error sending session delete message to session layer")]
133    SessionDeleteMessageSendFailed,
134    #[error("error sending data message to application")]
135    ApplicationMessageSendFailed,
136    #[error("error sending data message to slim")]
137    SlimMessageSendFailed,
138    #[error("send failure reported from slim: {ctx}")]
139    SlimSendFailure { ctx: Box<ErrorPayload> },
140
141    // Session lifecycle and state (continued)
142    #[error("session is draining - drop message")]
143    SessionDrainingDrop,
144    #[error("session already closed")]
145    SessionAlreadyClosed,
146    #[error("participant is offline")]
147    ParticipantOffLine,
148    #[error("rejoin failed: epoch mismatch")]
149    RejoinFailed,
150    #[error("moderator not found")]
151    ModeratorNotFound,
152    #[error("another status change is already in progress")]
153    StatusChangeInProgress,
154    #[error("session cleanup failed: {details}")]
155    SessionCleanupFailed { details: String },
156    #[error("message send retries exhausted for id={id}")]
157    MessageSendRetryFailed { id: u32 },
158    #[error("message receive retries exhausted for id={id}")]
159    MessageReceiveRetryFailed { id: u32 },
160    #[error("session sender is shutdown, cannot send messages")]
161    SessionSenderShutdown,
162    #[error("session receiver is shutdown, cannot receive messages")]
163    SessionReceiverShutdown,
164    #[error("missing participant name on timer")]
165    MissingParticipantNameOnTimer,
166
167    // Message construction and extraction contexts
168    #[error("missing payload: {context}")]
169    MissingPayload { context: &'static str },
170    #[error("message build failed: {0}")]
171    MessageBuild(MessageError),
172    #[error("message payload extract failed in {context}: {source}")]
173    PayloadExtract {
174        context: &'static str,
175        source: MessageError,
176    },
177
178    // Participant connectivity
179    #[error("missing mls payload in welcome message")]
180    WelcomeMessageMissingMlsPayload,
181    #[error("invalid join request payload")]
182    InvalidJoinRequestPayload,
183    #[error("participant disconnected: {0}")]
184    ParticipantDisconnected(ProtoName),
185    #[error("missing participant name on disconnection event")]
186    MissingParticipantNameOnDisconnection,
187
188    // Moderator task orchestration
189    #[error("no pending requests for the given key: {0}")]
190    TimerNotFound(u32),
191    #[error("phase not supported for task")]
192    ModeratorTaskUnsupportedPhase,
193    #[error("unexpected timer id: {0}")]
194    ModeratorTaskUnexpectedTimerId(u32),
195    #[error("failed to add participant to session: {source}")]
196    ModeratorTaskAddFailed { source: Box<SessionError> },
197    #[error("failed to remove participant from session: {source}")]
198    ModeratorTaskRemoveFailed { source: Box<SessionError> },
199    #[error("failed to update session: {source}")]
200    ModeratorTaskUpdateFailed { source: Box<SessionError> },
201    #[error("failed to close session: {source}")]
202    ModeratorTaskCloseFailed { source: Box<SessionError> },
203}
204
205impl SessionError {
206    // Helper constructors for structured mapping without repeating strings.
207    pub fn build_error(err: MessageError) -> Self {
208        SessionError::MessageBuild(err)
209    }
210    pub fn extract_error(context: &'static str, err: MessageError) -> Self {
211        SessionError::PayloadExtract {
212            context,
213            source: err,
214        }
215    }
216    pub fn cleanup_failed<E: std::fmt::Display>(e: E) -> Self {
217        SessionError::SessionCleanupFailed {
218            details: e.to_string(),
219        }
220    }
221
222    // Helpers to construct new structured retry failure variants
223    pub fn send_retry_failed(id: u32) -> Self {
224        SessionError::MessageSendRetryFailed { id }
225    }
226
227    pub fn receive_retry_failed(id: u32) -> Self {
228        SessionError::MessageReceiveRetryFailed { id }
229    }
230
231    /// Extract session context from SlimSendFailure error
232    /// Returns None if the error is not a SlimSendFailure or if it lacks session context
233    pub fn session_context(&self) -> Option<&MessageContext> {
234        match self {
235            SessionError::SlimSendFailure { ctx } => ctx.session_context.as_ref(),
236            _ => None,
237        }
238    }
239
240    /// Check if this error is for a command message
241    pub fn is_command_message_error(&self) -> bool {
242        self.session_context()
243            .map(|ctx| ctx.get_session_message_type().is_command_message())
244            .unwrap_or(false)
245    }
246}