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