Skip to main content

synapse_rpc/
error.rs

1//! RPC error types for service handlers
2//!
3//! Services define their own error types using thiserror, then implement
4//! `Into<ServiceError>` (or derive it with `#[derive(RpcError)]`).
5//!
6//! # Example
7//!
8//! ```ignore
9//! use synapse_rpc::{RpcError, ServiceError};
10//! use synapse_proto::RpcStatus;
11//!
12//! #[derive(Debug, thiserror::Error, RpcError)]
13//! pub enum UserError {
14//!     #[error("user not found: {0}")]
15//!     #[rpc(status = "Error", code = 404)]
16//!     NotFound(String),
17//!
18//!     #[error("invalid email format")]
19//!     #[rpc(status = "InvalidRequest", code = 400)]
20//!     InvalidEmail,
21//!
22//!     #[error("user is banned")]
23//!     #[rpc(code = 3001)]  // defaults to Error status
24//!     Banned,
25//! }
26//! ```
27
28use std::fmt;
29use synapse_proto::RpcStatus;
30
31/// Result type for RPC handlers
32///
33/// The error type must implement `Into<ServiceError>`.
34pub type RpcResult<T, E = ServiceError> = Result<T, E>;
35
36/// Error type that maps to RPC response errors
37///
38/// Services can either use this directly or define their own error types
39/// that implement `Into<ServiceError>`.
40#[derive(Debug, Clone)]
41pub struct ServiceError {
42    /// RPC status category
43    pub status: RpcStatus,
44    /// Error code (maps to RpcStatus or custom service codes)
45    pub code: u32,
46    /// Human-readable error message
47    pub message: String,
48}
49
50impl ServiceError {
51    /// Create a new service error
52    pub fn new(status: RpcStatus, code: u32, message: impl Into<String>) -> Self {
53        Self {
54            status,
55            code,
56            message: message.into(),
57        }
58    }
59
60    // ========================================================================
61    // Common error constructors
62    // ========================================================================
63
64    /// Invalid request (bad input, validation failed) - code 400
65    pub fn invalid_request(message: impl Into<String>) -> Self {
66        Self::new(RpcStatus::InvalidRequest, 400, message)
67    }
68
69    /// Resource not found - code 404
70    pub fn not_found(message: impl Into<String>) -> Self {
71        Self::new(RpcStatus::Error, 404, message)
72    }
73
74    /// Unauthorized (authentication required) - code 401
75    pub fn unauthorized(message: impl Into<String>) -> Self {
76        Self::new(RpcStatus::Error, 401, message)
77    }
78
79    /// Forbidden (authenticated but not allowed) - code 403
80    pub fn forbidden(message: impl Into<String>) -> Self {
81        Self::new(RpcStatus::Error, 403, message)
82    }
83
84    /// Internal error (something went wrong) - code 500
85    pub fn internal(message: impl Into<String>) -> Self {
86        Self::new(RpcStatus::Error, 500, message)
87    }
88
89    /// Service unavailable - code 503
90    pub fn unavailable(message: impl Into<String>) -> Self {
91        Self::new(RpcStatus::Unavailable, 503, message)
92    }
93
94    /// Timeout - code 504
95    pub fn timeout(message: impl Into<String>) -> Self {
96        Self::new(RpcStatus::Timeout, 504, message)
97    }
98
99    /// Custom service-specific error
100    ///
101    /// Use codes starting from your service's allocated range.
102    /// See CLAUDE.md for error code ranges:
103    /// - 1000-1999: Gateway errors
104    /// - 2000-2999: Auth errors
105    /// - 3000-3999: User service errors
106    /// - 4000-4999: Payment service errors
107    pub fn custom(code: u32, message: impl Into<String>) -> Self {
108        Self::new(RpcStatus::Error, code, message)
109    }
110
111    /// Convert to proto RpcError
112    pub fn to_proto(&self) -> synapse_proto::RpcError {
113        synapse_proto::RpcError {
114            code: self.code,
115            message: self.message.clone(),
116            details: vec![],
117        }
118    }
119}
120
121impl fmt::Display for ServiceError {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "[{}] {}", self.code, self.message)
124    }
125}
126
127impl std::error::Error for ServiceError {}
128
129// ============================================================================
130// Trait for converting custom errors to ServiceError
131// ============================================================================
132
133/// Trait for errors that can be converted to RPC errors
134///
135/// Implement this for your custom error types, or use `#[derive(RpcError)]`.
136pub trait IntoServiceError {
137    /// Convert to ServiceError
138    fn into_service_error(self) -> ServiceError;
139}
140
141impl IntoServiceError for ServiceError {
142    fn into_service_error(self) -> ServiceError {
143        self
144    }
145}
146
147// ============================================================================
148// Conversions from common error types
149// ============================================================================
150
151impl From<std::io::Error> for ServiceError {
152    fn from(err: std::io::Error) -> Self {
153        Self::internal(format!("IO error: {}", err))
154    }
155}
156
157impl From<serde_json::Error> for ServiceError {
158    fn from(err: serde_json::Error) -> Self {
159        Self::invalid_request(format!("JSON error: {}", err))
160    }
161}
162
163impl From<anyhow::Error> for ServiceError {
164    fn from(err: anyhow::Error) -> Self {
165        Self::internal(err.to_string())
166    }
167}
168
169// ============================================================================
170// Transport-level errors (for codec, connection, etc.)
171// ============================================================================
172
173/// Transport-level RPC error (codec, connection, etc.)
174#[derive(Debug, thiserror::Error)]
175pub enum TransportError {
176    #[error("Interface not found: {0:?}")]
177    InterfaceNotFound(synapse_primitives::InterfaceId),
178
179    #[error("Method not found: {0:?}")]
180    MethodNotFound(synapse_primitives::MethodId),
181
182    #[error("IO error: {0}")]
183    Io(#[from] std::io::Error),
184
185    #[error("Codec error: {0}")]
186    Codec(String),
187
188    #[error("Timeout")]
189    Timeout,
190
191    #[error("Service unavailable")]
192    Unavailable,
193
194    #[error("Invalid request: {0}")]
195    InvalidRequest(String),
196
197    #[error("Other error: {0}")]
198    Other(String),
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    // ========== Constructors ==========
206
207    #[test]
208    fn test_invalid_request() {
209        let err = ServiceError::invalid_request("bad input");
210        assert_eq!(err.status, RpcStatus::InvalidRequest);
211        assert_eq!(err.code, 400);
212        assert_eq!(err.message, "bad input");
213    }
214
215    #[test]
216    fn test_not_found() {
217        let err = ServiceError::not_found("user 42");
218        assert_eq!(err.status, RpcStatus::Error);
219        assert_eq!(err.code, 404);
220        assert_eq!(err.message, "user 42");
221    }
222
223    #[test]
224    fn test_unauthorized() {
225        let err = ServiceError::unauthorized("no token");
226        assert_eq!(err.status, RpcStatus::Error);
227        assert_eq!(err.code, 401);
228    }
229
230    #[test]
231    fn test_forbidden() {
232        let err = ServiceError::forbidden("not allowed");
233        assert_eq!(err.status, RpcStatus::Error);
234        assert_eq!(err.code, 403);
235    }
236
237    #[test]
238    fn test_internal() {
239        let err = ServiceError::internal("something broke");
240        assert_eq!(err.status, RpcStatus::Error);
241        assert_eq!(err.code, 500);
242    }
243
244    #[test]
245    fn test_unavailable() {
246        let err = ServiceError::unavailable("down for maintenance");
247        assert_eq!(err.status, RpcStatus::Unavailable);
248        assert_eq!(err.code, 503);
249    }
250
251    #[test]
252    fn test_timeout() {
253        let err = ServiceError::timeout("took too long");
254        assert_eq!(err.status, RpcStatus::Timeout);
255        assert_eq!(err.code, 504);
256    }
257
258    #[test]
259    fn test_custom() {
260        let err = ServiceError::custom(3001, "user banned");
261        assert_eq!(err.status, RpcStatus::Error);
262        assert_eq!(err.code, 3001);
263        assert_eq!(err.message, "user banned");
264    }
265
266    // ========== to_proto ==========
267
268    #[test]
269    fn test_to_proto() {
270        let err = ServiceError::new(RpcStatus::InvalidRequest, 400, "bad");
271        let proto = err.to_proto();
272        assert_eq!(proto.code, 400);
273        assert_eq!(proto.message, "bad");
274        assert!(proto.details.is_empty());
275    }
276
277    // ========== Display ==========
278
279    #[test]
280    fn test_display() {
281        let err = ServiceError::not_found("item 7");
282        let display = format!("{}", err);
283        assert_eq!(display, "[404] item 7");
284    }
285
286    // ========== From impls ==========
287
288    #[test]
289    fn test_from_io_error() {
290        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
291        let err: ServiceError = io_err.into();
292        assert_eq!(err.code, 500);
293        assert!(err.message.contains("IO error"));
294    }
295
296    #[test]
297    fn test_from_serde_json_error() {
298        let json_err = serde_json::from_str::<String>("not json{{{").unwrap_err();
299        let err: ServiceError = json_err.into();
300        assert_eq!(err.code, 400);
301        assert_eq!(err.status, RpcStatus::InvalidRequest);
302        assert!(err.message.contains("JSON error"));
303    }
304
305    #[test]
306    fn test_from_anyhow_error() {
307        let anyhow_err = anyhow::anyhow!("something went wrong");
308        let err: ServiceError = anyhow_err.into();
309        assert_eq!(err.code, 500);
310        assert!(err.message.contains("something went wrong"));
311    }
312
313    // ========== IntoServiceError ==========
314
315    #[test]
316    fn test_into_service_error_identity() {
317        let original = ServiceError::custom(3001, "test");
318        let converted = original.clone().into_service_error();
319        assert_eq!(converted.code, 3001);
320        assert_eq!(converted.message, "test");
321    }
322
323    // ========== Clone ==========
324
325    #[test]
326    fn test_clone() {
327        let err = ServiceError::internal("oops");
328        let cloned = err.clone();
329        assert_eq!(cloned.code, err.code);
330        assert_eq!(cloned.message, err.message);
331    }
332}