razor_stream/error.rs
1//! Error handling for razor-stream RPC framework.
2//!
3//! This module provides the error types and traits used for RPC error handling.
4//! It supports both internal RPC errors and user-defined custom errors.
5//!
6//! # Default Supported Error Types
7//!
8//! The following types have built-in `RpcErrCodec` implementations:
9//!
10//! - **Numeric types**: `i8`, `u8`, `i16`, `u16`, `i32`, `u32`
11//! - Encoded as `u32` values for efficient transport
12//! - Useful for errno-style error codes
13//!
14//! - **`()` (unit type)**
15//! - Encoded as `0u32`
16//! - Used when no additional error information is needed
17//!
18//! - **`String`**
19//! - Encoded as UTF-8 bytes
20//! - Useful for descriptive error messages
21//!
22//! - **`nix::errno::Errno`**
23//! - Encoded as `u32` values
24//! - For system-level error codes
25//!
26//! # Custom Error Types
27//!
28//! To use your own error type in RPC methods, implement the [`RpcErrCodec`] trait.
29//! There are two common approaches:
30//!
31//! ## Approach 1: Numeric Encoding (errno-style)
32//!
33//! Use this when you want compact, efficient numeric error codes.
34//!
35//! ```rust
36//! use num_enum::TryFromPrimitive;
37//! use razor_stream::{Codec, error::{RpcErrCodec, RpcIntErr, EncodedErr}};
38//!
39//! #[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)]
40//! #[repr(u32)]
41//! pub enum MyErrorCode {
42//! NotFound = 1,
43//! PermissionDenied = 2,
44//! Timeout = 3,
45//! }
46//!
47//! impl RpcErrCodec for MyErrorCode {
48//! fn encode<C: Codec>(&self, _codec: &C) -> EncodedErr {
49//! EncodedErr::Num(*self as u32)
50//! }
51//!
52//! fn decode<C: Codec>(_codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
53//! if let Ok(code) = buf {
54//! return MyErrorCode::try_from(code).map_err(|_| ());
55//! }
56//! Err(())
57//! }
58//!
59//! fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
60//! std::fmt::Debug::fmt(self, f)
61//! }
62//! }
63//! ```
64//!
65//! ## Approach 2: String Encoding (with strum)
66//!
67//! Use this when you want human-readable error strings that can be serialized/deserialized.
68//! Uses `EncodedErr::Static` to avoid heap allocation during encoding.
69//!
70//! ```rust
71//! use razor_stream::{Codec, error::{RpcErrCodec, RpcIntErr, EncodedErr}};
72//! use std::str::FromStr;
73//! use strum::{Display, EnumString};
74//!
75//! #[derive(Debug, Clone, Display, EnumString, PartialEq)]
76//! pub enum MyStringError {
77//! #[strum(serialize = "not_found")]
78//! NotFound,
79//! #[strum(serialize = "permission_denied")]
80//! PermissionDenied,
81//! #[strum(serialize = "timeout")]
82//! Timeout,
83//! }
84//!
85//! impl MyStringError {
86//! /// Returns the static string representation for each variant.
87//! /// This avoids the overhead of `to_string()` allocation.
88//! fn as_static_str(&self) -> &'static str {
89//! match self {
90//! Self::NotFound => "not_found",
91//! Self::PermissionDenied => "permission_denied",
92//! Self::Timeout => "timeout",
93//! }
94//! }
95//! }
96//!
97//! impl RpcErrCodec for MyStringError {
98//! fn encode<C: Codec>(&self, _codec: &C) -> EncodedErr {
99//! // Use EncodedErr::Static to avoid heap allocation
100//! EncodedErr::Static(self.as_static_str())
101//! }
102//!
103//! fn decode<C: Codec>(_codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
104//! // Decode with zero-copy: directly parse from &[u8] without allocating
105//! if let Err(bytes) = buf {
106//! // Safety: error strings are valid ASCII (subset of UTF-8), so unchecked is safe
107//! let s = unsafe { std::str::from_utf8_unchecked(bytes) };
108//! // Use strum's EnumString derive to parse from string
109//! return MyStringError::from_str(s).map_err(|_| ());
110//! }
111//! Err(())
112//! }
113//!
114//! fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
115//! std::fmt::Display::fmt(self, f)
116//! }
117//! }
118//! ```
119
120use crate::Codec;
121use std::fmt;
122
123/// "rpc_" prefix is reserved for internal error, you should avoid conflict with it
124pub const RPC_ERR_PREFIX: &str = "rpc_";
125
126/// A error type defined by client-side user logic
127///
128/// Due to possible decode
129#[derive(thiserror::Error)]
130pub enum RpcError<E: RpcErrCodec> {
131 User(E),
132 Rpc(RpcIntErr),
133}
134
135impl<E: RpcErrCodec> fmt::Display for RpcError<E> {
136 #[inline]
137 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138 match self {
139 Self::User(e) => RpcErrCodec::fmt(e, f),
140 Self::Rpc(e) => fmt::Display::fmt(e, f),
141 }
142 }
143}
144
145impl<E: RpcErrCodec> fmt::Debug for RpcError<E> {
146 #[inline]
147 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148 fmt::Display::fmt(self, f)
149 }
150}
151
152impl<E: RpcErrCodec> std::cmp::PartialEq<RpcIntErr> for RpcError<E> {
153 #[inline]
154 fn eq(&self, other: &RpcIntErr) -> bool {
155 if let Self::Rpc(r) = self
156 && r == other
157 {
158 return true;
159 }
160 false
161 }
162}
163
164impl<E: RpcErrCodec + PartialEq> std::cmp::PartialEq<E> for RpcError<E> {
165 #[inline]
166 fn eq(&self, other: &E) -> bool {
167 if let Self::User(r) = self {
168 return r == other;
169 }
170 false
171 }
172}
173
174impl<E: RpcErrCodec + PartialEq> std::cmp::PartialEq<RpcError<E>> for RpcError<E> {
175 #[inline]
176 fn eq(&self, other: &Self) -> bool {
177 match self {
178 Self::Rpc(r) => {
179 if let Self::Rpc(o) = other {
180 return r == o;
181 }
182 }
183 Self::User(r) => {
184 if let Self::User(o) = other {
185 return r == o;
186 }
187 }
188 }
189 false
190 }
191}
192
193impl<E: RpcErrCodec> From<E> for RpcError<E> {
194 #[inline]
195 fn from(e: E) -> Self {
196 Self::User(e)
197 }
198}
199
200impl From<&str> for RpcError<String> {
201 #[inline]
202 fn from(e: &str) -> Self {
203 Self::User(e.to_string())
204 }
205}
206
207impl<E: RpcErrCodec> From<RpcIntErr> for RpcError<E> {
208 #[inline]
209 fn from(e: RpcIntErr) -> Self {
210 Self::Rpc(e)
211 }
212}
213
214/// Serialize and Deserialize trait for custom RpcError
215///
216/// There is only two forms for rpc transport layer, u32 and String, choose one of them.
217///
218/// Because Rust does not allow overlapping impl, we only imple RpcErrCodec trait by default for the following types:
219/// - ()
220/// - from i8 to u32
221/// - String
222/// - nix::errno::Errno
223///
224/// If you use other type as error, you can implement manually:
225///
226/// # Example with serde_derive
227/// ```rust
228/// use serde_derive::{Serialize, Deserialize};
229/// use razor_stream::{Codec, error::{RpcErrCodec, RpcIntErr, EncodedErr}};
230/// use strum::Display;
231/// #[derive(Serialize, Deserialize, Debug)]
232/// pub enum MyError {
233/// NoSuchFile,
234/// TooManyRequest,
235/// }
236///
237/// impl RpcErrCodec for MyError {
238/// #[inline(always)]
239/// fn encode<C: Codec>(&self, codec: &C) -> EncodedErr {
240/// match codec.encode(self) {
241/// Ok(buf)=>EncodedErr::Buf(buf),
242/// Err(())=>EncodedErr::Rpc(RpcIntErr::Encode),
243/// }
244/// }
245///
246/// #[inline(always)]
247/// fn decode<C: Codec>(codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
248/// if let Err(b) = buf {
249/// return codec.decode(b);
250/// } else {
251/// Err(())
252/// }
253/// }
254/// #[inline(always)]
255/// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
256/// std::fmt::Debug::fmt(self, f)
257/// }
258/// }
259/// ```
260///
261/// # Example with num_enum
262///
263/// ```rust
264/// use num_enum::TryFromPrimitive;
265/// use razor_stream::{Codec, error::{RpcErrCodec, RpcIntErr, EncodedErr}};
266///
267/// // Define your error codes as a C-like enum with explicit values
268/// // You can use num_enum's TryFromPrimitive for safer deserialization
269/// #[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)]
270/// #[repr(u32)]
271/// pub enum MyRpcErrorCode {
272/// /// Service is not available
273/// ServiceUnavailable = 1,
274/// /// Request timed out
275/// RequestTimeout = 2,
276/// /// Invalid parameter
277/// InvalidParameter = 3,
278/// /// Resource not found
279/// NotFound = 4,
280/// }
281///
282/// impl RpcErrCodec for MyRpcErrorCode {
283/// #[inline(always)]
284/// fn encode<C: Codec>(&self, _codec: &C) -> EncodedErr {
285/// // Manual conversion to u32 (no IntoPrimitive needed)
286/// let code: u32 = *self as u32;
287/// EncodedErr::Num(code)
288/// }
289///
290/// #[inline(always)]
291/// fn decode<C: Codec>(_codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
292/// if let Ok(code) = buf {
293/// // Using num_enum for safe deserialization (TryFromPrimitive)
294/// return MyRpcErrorCode::try_from(code).map_err(|_| ());
295/// }
296/// Err(())
297/// }
298///
299/// #[inline(always)]
300/// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
301/// std::fmt::Debug::fmt(self, f)
302/// }
303/// }
304/// ```
305pub trait RpcErrCodec: Send + Sized + 'static + Unpin {
306 fn encode<C: Codec>(&self, codec: &C) -> EncodedErr;
307
308 fn decode<C: Codec>(codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()>;
309
310 /// You can choose to use std::fmt::Debug or std::fmt::Display for the type.
311 ///
312 /// NOTE that this method exists because rust does not have Display for ().
313 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result;
314}
315
316macro_rules! impl_rpc_error_for_num {
317 ($t: tt) => {
318 impl RpcErrCodec for $t {
319 #[inline(always)]
320 fn encode<C: Codec>(&self, _codec: &C) -> EncodedErr {
321 EncodedErr::Num(*self as u32)
322 }
323
324 #[inline(always)]
325 fn decode<C: Codec>(_codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
326 if let Ok(i) = buf {
327 if i <= $t::MAX as u32 {
328 return Ok(i as Self);
329 }
330 }
331 Err(())
332 }
333
334 #[inline(always)]
335 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
336 write!(f, "errno {}", self)
337 }
338 }
339 };
340}
341
342impl_rpc_error_for_num!(i8);
343impl_rpc_error_for_num!(u8);
344impl_rpc_error_for_num!(i16);
345impl_rpc_error_for_num!(u16);
346impl_rpc_error_for_num!(i32);
347impl_rpc_error_for_num!(u32);
348
349impl RpcErrCodec for nix::errno::Errno {
350 #[inline(always)]
351 fn encode<C: Codec>(&self, _codec: &C) -> EncodedErr {
352 EncodedErr::Num(*self as u32)
353 }
354
355 #[inline(always)]
356 fn decode<C: Codec>(_codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
357 if let Ok(i) = buf
358 && i <= i32::MAX as u32
359 {
360 return Ok(Self::from_raw(i as i32));
361 }
362 Err(())
363 }
364
365 #[inline(always)]
366 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
367 write!(f, "{:?}", self)
368 }
369}
370
371impl RpcErrCodec for () {
372 #[inline(always)]
373 fn encode<C: Codec>(&self, _codec: &C) -> EncodedErr {
374 EncodedErr::Num(0u32)
375 }
376
377 #[inline(always)]
378 fn decode<C: Codec>(_codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
379 if let Ok(i) = buf
380 && i == 0
381 {
382 return Ok(());
383 }
384 Err(())
385 }
386
387 #[inline(always)]
388 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
389 write!(f, "err")
390 }
391}
392
393impl RpcErrCodec for String {
394 #[inline(always)]
395 fn encode<C: Codec>(&self, _codec: &C) -> EncodedErr {
396 EncodedErr::Buf(Vec::from(self.as_bytes()))
397 }
398 #[inline(always)]
399 fn decode<C: Codec>(_codec: &C, buf: Result<u32, &[u8]>) -> Result<Self, ()> {
400 if let Err(s) = buf
401 && let Ok(s) = str::from_utf8(s)
402 {
403 return Ok(s.to_string());
404 }
405 Err(())
406 }
407
408 #[inline(always)]
409 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
410 write!(f, "{}", self)
411 }
412}
413
414/// RpcIntErr represent internal error from the framework
415///
416/// **NOTE**:
417/// - This error type is serialized in string, "rpc_" prefix is reserved for internal error, you
418/// should avoid conflict with it.
419/// - We presume the variants less than RpcIntErr::Method is retriable errors
420#[derive(
421 strum::Display,
422 strum::EnumString,
423 strum::AsRefStr,
424 PartialEq,
425 PartialOrd,
426 Clone,
427 thiserror::Error,
428)]
429#[repr(u8)]
430pub enum RpcIntErr {
431 /// Ping or connect error
432 #[strum(serialize = "rpc_unreachable")]
433 Unreachable = 0,
434 /// IO error
435 #[strum(serialize = "rpc_io_err")]
436 IO = 1,
437 /// Task timeout
438 #[strum(serialize = "rpc_timeout")]
439 Timeout = 2,
440 /// Method not found
441 #[strum(serialize = "rpc_method_notfound")]
442 Method = 3,
443 /// service notfound
444 #[strum(serialize = "rpc_service_notfound")]
445 Service = 4,
446 /// Encode Error
447 #[strum(serialize = "rpc_encode")]
448 Encode = 5,
449 /// Decode Error
450 #[strum(serialize = "rpc_decode")]
451 Decode = 6,
452 /// Internal error
453 #[strum(serialize = "rpc_internal_err")]
454 Internal = 7,
455 /// invalid version number in rpc header
456 #[strum(serialize = "rpc_invalid_ver")]
457 Version = 8,
458}
459
460// The default Debug derive just ignore strum customized string, by strum only have a Display derive
461impl fmt::Debug for RpcIntErr {
462 #[inline]
463 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
464 fmt::Display::fmt(self, f)
465 }
466}
467
468impl RpcIntErr {
469 #[inline]
470 pub fn as_bytes(&self) -> &[u8] {
471 self.as_ref().as_bytes()
472 }
473}
474
475impl From<std::io::Error> for RpcIntErr {
476 #[inline(always)]
477 fn from(_e: std::io::Error) -> Self {
478 Self::IO
479 }
480}
481
482/// A container for error message parse from / send into transport
483#[derive(Debug, thiserror::Error)]
484pub enum EncodedErr {
485 /// The ClientTransport should try the best to parse it from string with "rpc_" prefix
486 Rpc(RpcIntErr),
487 /// For nix errno and the like
488 Num(u32),
489 /// only for server, the ClientTransport will not parse into static type
490 Static(&'static str),
491 /// The ClientTransport will fallback to `Vec<u8>` after try to parse RpcIntErr and num
492 Buf(Vec<u8>),
493}
494
495impl EncodedErr {
496 #[inline]
497 pub fn try_as_str(&self) -> Result<&str, ()> {
498 match self {
499 Self::Static(s) => return Ok(s),
500 Self::Buf(b) => {
501 if let Ok(s) = str::from_utf8(b) {
502 return Ok(s);
503 }
504 }
505 _ => {}
506 }
507 Err(())
508 }
509}
510
511/// Just for macro test
512impl std::cmp::PartialEq<EncodedErr> for EncodedErr {
513 fn eq(&self, other: &EncodedErr) -> bool {
514 match self {
515 Self::Rpc(e) => {
516 if let Self::Rpc(o) = other {
517 return e == o;
518 }
519 }
520 Self::Num(e) => {
521 if let Self::Num(o) = other {
522 return e == o;
523 }
524 }
525 Self::Static(s) => {
526 if let Ok(o) = other.try_as_str() {
527 return *s == o;
528 }
529 }
530 Self::Buf(s) => {
531 if let Self::Buf(o) = other {
532 return s == o;
533 } else if let Ok(o) = other.try_as_str() {
534 // other's type is not Buf
535 if let Ok(_s) = str::from_utf8(s) {
536 return _s == o;
537 }
538 }
539 }
540 }
541 false
542 }
543}
544
545impl fmt::Display for EncodedErr {
546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547 match self {
548 Self::Rpc(e) => e.fmt(f),
549 Self::Num(no) => write!(f, "errno {}", no),
550 Self::Static(s) => write!(f, "{}", s),
551 Self::Buf(b) => match str::from_utf8(b) {
552 Ok(s) => {
553 write!(f, "{}", s)
554 }
555 Err(_) => {
556 write!(f, "err blob {} length", b.len())
557 }
558 },
559 }
560 }
561}
562
563impl From<RpcIntErr> for EncodedErr {
564 #[inline(always)]
565 fn from(e: RpcIntErr) -> Self {
566 Self::Rpc(e)
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use nix::errno::Errno;
574 use std::str::FromStr;
575
576 #[test]
577 fn test_internal_error() {
578 println!("{}", RpcIntErr::Internal);
579 println!("{:?}", RpcIntErr::Internal);
580 let s = RpcIntErr::Timeout.as_ref();
581 println!("RpcIntErr::Timeout as {}", s);
582 let e = RpcIntErr::from_str(s).expect("parse");
583 assert_eq!(e, RpcIntErr::Timeout);
584 assert!(RpcIntErr::from_str("timeoutss").is_err());
585 assert!(RpcIntErr::Timeout < RpcIntErr::Method);
586 assert!(RpcIntErr::IO < RpcIntErr::Method);
587 assert!(RpcIntErr::Unreachable < RpcIntErr::Method);
588 }
589
590 #[test]
591 fn test_rpc_error_default() {
592 let e = RpcError::<i32>::from(1i32);
593 println!("err {:?} {}", e, e);
594
595 let e = RpcError::<Errno>::from(Errno::EIO);
596 println!("err {:?} {}", e, e);
597
598 let e = RpcError::<String>::from("err_str");
599 println!("err {:?} {}", e, e);
600 let e2 = RpcError::<String>::from("err_str".to_string());
601 assert_eq!(e, e2);
602
603 let e = RpcError::<String>::from(RpcIntErr::IO);
604 println!("err {:?} {}", e, e);
605
606 let _e: Result<(), RpcIntErr> = Err(RpcIntErr::IO);
607
608 // let e: Result<(), RpcError::<String>> = _e.into();
609 // Not allow by rust, and orphan rule prevent we do
610 // `impl<E: RpcErrCodec> From<Result<(), RpcIntErr>> for Result<(), RpcError<E>>`
611
612 // it's ok to use map_err
613 let e: Result<(), RpcError<String>> = _e.map_err(|e| e.into());
614 println!("err {:?}", e);
615 }
616
617 //#[test]
618 //fn test_rpc_error_string_enum() {
619 // not supported by default, should provide a derive
620 // #[derive(
621 // Debug, strum::Display, strum::EnumString, strum::AsRefStr, PartialEq, Clone, thiserror::Error,
622 // )]
623 // enum MyError {
624 // OhMyGod,
625 // }
626 // let e = RpcError::<MyError>::from(MyError::OhMyGod);
627 // println!("err {:?} {}", e, e);
628 //}
629}