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