Skip to main content

ant_quic/
error_handling.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Standardized Error Handling Patterns for ant-quic
9//!
10//! This module provides consistent error handling patterns and utilities
11//! to ensure uniform error propagation and handling across the codebase.
12
13use thiserror::Error;
14
15/// Comprehensive error type for ant-quic operations
16#[derive(Error, Debug)]
17pub enum AntQuicError {
18    /// Transport-level errors (connection issues, protocol violations)
19    #[error("Transport error: {0}")]
20    Transport(#[from] crate::transport_error::Error),
21
22    /// Connection establishment errors
23    #[error("Connection error: {0}")]
24    Connection(#[from] crate::connection::ConnectionError),
25
26    /// Network address discovery errors
27    #[error("Discovery error: {0}")]
28    Discovery(#[from] crate::candidate_discovery::DiscoveryError),
29
30    /// NAT traversal errors
31    #[error("NAT traversal error: {0}")]
32    NatTraversal(#[from] crate::nat_traversal_api::NatTraversalError),
33
34    /// Configuration validation errors
35    #[error("Configuration error: {0}")]
36    Config(String),
37
38    /// I/O operation errors
39    #[error("I/O error: {0}")]
40    Io(#[from] std::io::Error),
41
42    /// Cryptographic operation errors
43    #[error("Crypto error: {0}")]
44    Crypto(String),
45
46    /// Post-Quantum Cryptography errors
47    #[error("PQC error: {0}")]
48    Pqc(#[from] crate::crypto::pqc::types::PqcError),
49
50    /// Timeout errors
51    #[error("Operation timed out: {0}")]
52    Timeout(String),
53
54    /// Resource exhaustion errors
55    #[error("Resource exhausted: {0}")]
56    ResourceExhausted(String),
57
58    /// Invalid input parameters
59    #[error("Invalid parameter: {0}")]
60    InvalidParameter(String),
61
62    /// Internal errors (should not happen in production)
63    #[error("Internal error: {0}")]
64    Internal(String),
65}
66
67/// Result type alias for ant-quic operations
68pub type Result<T> = std::result::Result<T, AntQuicError>;
69
70/// Error handling utilities
71pub mod utils {
72    use super::*;
73    use tracing::{debug, error, info, warn};
74
75    /// Log an error with appropriate level based on severity
76    pub fn log_error(error: &(dyn std::error::Error + 'static), context: &str) {
77        let error_msg = format!("{}: {}", context, error);
78        match error.downcast_ref::<AntQuicError>() {
79            Some(AntQuicError::Internal(_)) => error!("{}", error_msg),
80            Some(AntQuicError::Transport(_)) => warn!("{}", error_msg),
81            Some(AntQuicError::Connection(_)) => warn!("{}", error_msg),
82            Some(AntQuicError::Timeout(_)) => info!("{}", error_msg),
83            Some(AntQuicError::InvalidParameter(_)) => debug!("{}", error_msg),
84            _ => warn!("{}", error_msg),
85        }
86    }
87
88    /// Convert an error to a user-friendly message
89    pub fn to_user_message(error: &(dyn std::error::Error + 'static)) -> String {
90        match error.downcast_ref::<AntQuicError>() {
91            Some(AntQuicError::Transport(_)) => {
92                "Network connection error. Please check your internet connection.".to_string()
93            }
94            Some(AntQuicError::Connection(_)) => {
95                "Failed to establish connection. The remote peer may be unreachable.".to_string()
96            }
97            Some(AntQuicError::Discovery(_)) => {
98                "Failed to discover network configuration. Please check your network settings."
99                    .to_string()
100            }
101            Some(AntQuicError::NatTraversal(_)) => {
102                "NAT traversal failed. This may be due to restrictive network policies.".to_string()
103            }
104            Some(AntQuicError::Timeout(_)) => "Operation timed out. Please try again.".to_string(),
105            Some(AntQuicError::Config(_)) => {
106                "Configuration error. Please check your settings.".to_string()
107            }
108            Some(AntQuicError::Io(_)) => {
109                "System I/O error. Please check file permissions and disk space.".to_string()
110            }
111            Some(AntQuicError::Crypto(_)) => {
112                "Cryptographic operation failed. This may indicate a security issue.".to_string()
113            }
114            Some(AntQuicError::Pqc(_)) => {
115                "Post-quantum cryptographic operation failed.".to_string()
116            }
117            Some(AntQuicError::ResourceExhausted(_)) => {
118                "System resources exhausted. Please close some applications and try again."
119                    .to_string()
120            }
121            Some(AntQuicError::InvalidParameter(_)) => {
122                "Invalid input parameters provided.".to_string()
123            }
124            Some(AntQuicError::Internal(_)) => {
125                "An internal error occurred. Please report this issue.".to_string()
126            }
127            _ => format!("An unexpected error occurred: {}", error),
128        }
129    }
130
131    /// Check if an error is recoverable
132    pub fn is_recoverable(error: &(dyn std::error::Error + 'static)) -> bool {
133        match error.downcast_ref::<AntQuicError>() {
134            Some(AntQuicError::Timeout(_)) => true,
135            Some(AntQuicError::Connection(_)) => true,
136            Some(AntQuicError::Discovery(_)) => true,
137            Some(AntQuicError::NatTraversal(_)) => true,
138            Some(AntQuicError::Io(io_err)) => {
139                // Some I/O errors are recoverable
140                matches!(
141                    io_err.kind(),
142                    std::io::ErrorKind::TimedOut | std::io::ErrorKind::Interrupted
143                )
144            }
145            _ => false,
146        }
147    }
148
149    /// Get recommended retry delay for an error
150    pub fn get_retry_delay(
151        error: &(dyn std::error::Error + 'static),
152    ) -> Option<std::time::Duration> {
153        match error.downcast_ref::<AntQuicError>() {
154            Some(AntQuicError::Timeout(_)) => Some(std::time::Duration::from_millis(100)),
155            Some(AntQuicError::Connection(_)) => Some(std::time::Duration::from_millis(500)),
156            Some(AntQuicError::Discovery(_)) => Some(std::time::Duration::from_secs(1)),
157            Some(AntQuicError::NatTraversal(_)) => Some(std::time::Duration::from_secs(2)),
158            Some(AntQuicError::Io(io_err)) => match io_err.kind() {
159                std::io::ErrorKind::TimedOut => Some(std::time::Duration::from_millis(100)),
160                std::io::ErrorKind::Interrupted => Some(std::time::Duration::from_millis(10)),
161                _ => None,
162            },
163            _ => None,
164        }
165    }
166}
167
168/// Checks a condition and returns an error (converted via `.into()`) if it's false.
169#[macro_export]
170macro_rules! ensure {
171    ($condition:expr, $error:expr) => {
172        if !($condition) {
173            return Err($error.into());
174        }
175    };
176}
177
178/// Returns an error immediately, converting via .
179/// Returns an error immediately, converting via `.into()`.
180#[macro_export]
181macro_rules! bail {
182    ($error:expr) => {
183        return Err($error.into());
184    };
185}
186
187/// Wraps a Result's error with additional context, converting to [].
188/// Wraps a Result's error with additional context, converting to [`AntQuicError::Internal`].
189#[macro_export]
190macro_rules! context {
191    ($result:expr, $context:expr) => {
192        $result.map_err(|e| AntQuicError::Internal(format!("{}: {}", $context, e)))
193    };
194}
195
196/// Best practices for error handling:
197///
198/// 1. **Use Result<T, E> everywhere**: Never use unwrap() or expect() in production code
199/// 2. **Chain errors with ? operator**: Let errors bubble up naturally
200/// 3. **Add context when needed**: Use context! macro to add context to errors
201/// 4. **Handle recoverable errors**: Use is_recoverable() to determine if retry is appropriate
202/// 5. **Log errors appropriately**: Use log_error() for consistent error logging
203/// 6. **Provide user-friendly messages**: Use to_user_message() for end-user communication
204/// 7. **Use specific error types**: Prefer specific error variants over generic ones
205/// 8. **Document error conditions**: Document when and why errors can occur
206/// 9. **Test error paths**: Ensure error conditions are tested
207/// 10. **Fail securely**: Don't leak sensitive information in error messages
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    // ── AntQuicError construction and display ──
213
214    #[test]
215    fn error_transport_display() {
216        let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR("test"));
217        let display = format!("{err}");
218        assert!(display.contains("Transport error"));
219    }
220
221    #[test]
222    fn error_connection_display() {
223        let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
224        let display = format!("{err}");
225        assert!(display.contains("Connection error"));
226    }
227
228    #[test]
229    fn error_config_display() {
230        let err = AntQuicError::Config("bad config".to_string());
231        let display = format!("{err}");
232        assert!(display.contains("Configuration error"));
233        assert!(display.contains("bad config"));
234    }
235
236    #[test]
237    fn error_crypto_display() {
238        let err = AntQuicError::Crypto("key error".to_string());
239        let display = format!("{err}");
240        assert!(display.contains("Crypto error"));
241    }
242
243    #[test]
244    fn error_timeout_display() {
245        let err = AntQuicError::Timeout("timed out".to_string());
246        let display = format!("{err}");
247        assert!(display.contains("timed out"));
248    }
249
250    #[test]
251    fn error_resource_exhausted_display() {
252        let err = AntQuicError::ResourceExhausted("no mem".to_string());
253        let display = format!("{err}");
254        assert!(display.contains("Resource exhausted"));
255    }
256
257    #[test]
258    fn error_invalid_parameter_display() {
259        let err = AntQuicError::InvalidParameter("bad param".to_string());
260        let display = format!("{err}");
261        assert!(display.contains("Invalid parameter"));
262    }
263
264    #[test]
265    fn error_internal_display() {
266        let err = AntQuicError::Internal("bug".to_string());
267        let display = format!("{err}");
268        assert!(display.contains("Internal error"));
269    }
270
271    #[test]
272    fn error_debug_format() {
273        let err = AntQuicError::Internal("test".to_string());
274        let debug = format!("{err:?}");
275        assert!(debug.contains("Internal"));
276    }
277
278    #[test]
279    fn error_from_invalid_parameter() {
280        let s = "bad".to_string();
281        let err: AntQuicError = AntQuicError::InvalidParameter(s);
282        assert!(format!("{err}").contains("Invalid parameter"));
283    }
284
285    #[test]
286    fn error_io_into() {
287        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
288        let err: AntQuicError = io_err.into();
289        assert!(matches!(err, AntQuicError::Io(_)));
290    }
291
292    // ── utils::log_error tests ──
293
294    #[test]
295    fn log_error_internal() {
296        let err = AntQuicError::Internal("test internal".to_string());
297        // Should not panic
298        utils::log_error(&err, "test context");
299    }
300
301    #[test]
302    fn log_error_transport() {
303        let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR("x"));
304        utils::log_error(&err, "transport");
305    }
306
307    #[test]
308    fn log_error_generic() {
309        let err = std::io::Error::other("generic");
310        utils::log_error(&err, "generic context");
311    }
312
313    // ── utils::to_user_message tests ──
314
315    #[test]
316    fn user_message_transport() {
317        let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR(""));
318        let msg = utils::to_user_message(&err);
319        assert!(msg.contains("Network connection error"));
320    }
321
322    #[test]
323    fn user_message_connection() {
324        let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
325        let msg = utils::to_user_message(&err);
326        assert!(msg.contains("Failed to establish connection"));
327    }
328
329    #[test]
330    fn user_message_timeout() {
331        let err = AntQuicError::Timeout("x".to_string());
332        let msg = utils::to_user_message(&err);
333        assert!(msg.contains("timed out"));
334    }
335
336    #[test]
337    fn user_message_crypto() {
338        let err = AntQuicError::Crypto("x".to_string());
339        let msg = utils::to_user_message(&err);
340        assert!(msg.contains("Cryptographic operation failed"));
341    }
342
343    #[test]
344    fn user_message_internal() {
345        let err = AntQuicError::Internal("x".to_string());
346        let msg = utils::to_user_message(&err);
347        assert!(msg.contains("internal error"));
348    }
349
350    #[test]
351    fn user_message_config() {
352        let err = AntQuicError::Config("x".to_string());
353        let msg = utils::to_user_message(&err);
354        assert!(msg.contains("Configuration error"));
355    }
356
357    #[test]
358    fn user_message_discovery() {
359        // DiscoveryError is in candidate_discovery module — use a generic fallback
360        let err = std::io::Error::other("weird");
361        let msg = utils::to_user_message(&err);
362        assert!(msg.contains("unexpected error"));
363    }
364
365    #[test]
366    fn user_message_io() {
367        let err = AntQuicError::Io(std::io::Error::new(
368            std::io::ErrorKind::PermissionDenied,
369            "denied",
370        ));
371        let msg = utils::to_user_message(&err);
372        assert!(msg.contains("I/O error"));
373    }
374
375    #[test]
376    fn user_message_invalid_parameter() {
377        let err = AntQuicError::InvalidParameter("x".to_string());
378        let msg = utils::to_user_message(&err);
379        assert!(msg.contains("Invalid input"));
380    }
381
382    #[test]
383    fn user_message_resource_exhausted() {
384        let err = AntQuicError::ResourceExhausted("x".to_string());
385        let msg = utils::to_user_message(&err);
386        assert!(msg.contains("resources exhausted"));
387    }
388
389    #[test]
390    fn user_message_pqc() {
391        use crate::crypto::pqc::types::PqcError;
392        let err = AntQuicError::Pqc(PqcError::KeyGenerationFailed("test".to_string()));
393        let msg = utils::to_user_message(&err);
394        assert!(msg.contains("Post-quantum"));
395    }
396
397    #[test]
398    fn user_message_nat_traversal() {
399        use crate::nat_traversal_api::NatTraversalError;
400        let err = AntQuicError::NatTraversal(NatTraversalError::HolePunchingFailed);
401        let msg = utils::to_user_message(&err);
402        assert!(msg.contains("NAT traversal failed"));
403    }
404
405    // ── utils::is_recoverable tests ──
406
407    #[test]
408    fn timeout_is_recoverable() {
409        let err = AntQuicError::Timeout("x".to_string());
410        assert!(utils::is_recoverable(&err));
411    }
412
413    #[test]
414    fn connection_error_is_recoverable() {
415        let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
416        assert!(utils::is_recoverable(&err));
417    }
418
419    #[test]
420    fn internal_error_is_not_recoverable() {
421        let err = AntQuicError::Internal("x".to_string());
422        assert!(!utils::is_recoverable(&err));
423    }
424
425    #[test]
426    fn config_error_is_not_recoverable() {
427        let err = AntQuicError::Config("x".to_string());
428        assert!(!utils::is_recoverable(&err));
429    }
430
431    #[test]
432    fn io_timeout_is_recoverable() {
433        let err = AntQuicError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout"));
434        assert!(utils::is_recoverable(&err));
435    }
436
437    #[test]
438    fn io_interrupted_is_recoverable() {
439        let err = AntQuicError::Io(std::io::Error::new(
440            std::io::ErrorKind::Interrupted,
441            "interrupted",
442        ));
443        assert!(utils::is_recoverable(&err));
444    }
445
446    #[test]
447    fn io_permission_denied_is_not_recoverable() {
448        let err = AntQuicError::Io(std::io::Error::new(
449            std::io::ErrorKind::PermissionDenied,
450            "denied",
451        ));
452        assert!(!utils::is_recoverable(&err));
453    }
454
455    #[test]
456    fn generic_error_is_not_recoverable() {
457        let err = std::io::Error::other("other");
458        assert!(!utils::is_recoverable(&err));
459    }
460
461    #[test]
462    fn crypto_error_is_not_recoverable() {
463        let err = AntQuicError::Crypto("x".to_string());
464        assert!(!utils::is_recoverable(&err));
465    }
466
467    // ── utils::get_retry_delay tests ──
468
469    #[test]
470    fn timeout_has_retry_delay() {
471        let err = AntQuicError::Timeout("x".to_string());
472        let delay = utils::get_retry_delay(&err);
473        assert!(delay.is_some());
474        assert_eq!(delay.unwrap(), std::time::Duration::from_millis(100));
475    }
476
477    #[test]
478    fn connection_has_retry_delay() {
479        let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
480        let delay = utils::get_retry_delay(&err);
481        assert!(delay.is_some());
482        assert_eq!(delay.unwrap(), std::time::Duration::from_millis(500));
483    }
484
485    #[test]
486    fn discovery_has_retry_delay() {
487        use crate::candidate_discovery::DiscoveryError;
488        let err = AntQuicError::Discovery(DiscoveryError::NoLocalInterfaces);
489        let delay = utils::get_retry_delay(&err);
490        assert!(delay.is_some());
491    }
492
493    #[test]
494    fn internal_has_no_retry_delay() {
495        let err = AntQuicError::Internal("x".to_string());
496        assert!(utils::get_retry_delay(&err).is_none());
497    }
498
499    #[test]
500    fn generic_error_has_no_retry_delay() {
501        let err = std::io::Error::other("other");
502        assert!(utils::get_retry_delay(&err).is_none());
503    }
504
505    #[test]
506    fn io_timeout_has_retry_delay() {
507        let err = AntQuicError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout"));
508        let delay = utils::get_retry_delay(&err);
509        assert!(delay.is_some());
510        assert_eq!(delay.unwrap(), std::time::Duration::from_millis(100));
511    }
512
513    #[test]
514    fn io_interrupted_has_retry_delay() {
515        let err = AntQuicError::Io(std::io::Error::new(std::io::ErrorKind::Interrupted, "int"));
516        let delay = utils::get_retry_delay(&err);
517        assert!(delay.is_some());
518        assert_eq!(delay.unwrap(), std::time::Duration::from_millis(10));
519    }
520
521    // ── Macro tests ──
522
523    #[test]
524    fn ensure_passes_when_true() {
525        let result: Result<()> = (|| {
526            ensure!(
527                true,
528                AntQuicError::Internal("should not happen".to_string())
529            );
530            Ok(())
531        })();
532        assert!(result.is_ok());
533    }
534
535    #[test]
536    fn ensure_fails_when_false() {
537        let result: Result<()> = (|| {
538            ensure!(false, AntQuicError::InvalidParameter("bad".to_string()));
539            Ok(())
540        })();
541        assert!(result.is_err());
542        assert!(matches!(
543            result.unwrap_err(),
544            AntQuicError::InvalidParameter(_)
545        ));
546    }
547
548    #[test]
549    fn bail_returns_error() {
550        let result: Result<()> = (|| {
551            bail!(AntQuicError::Internal("bailed".to_string()));
552        })();
553        assert!(result.is_err());
554        assert!(format!("{}", result.unwrap_err()).contains("bailed"));
555    }
556
557    #[test]
558    fn bail_with_type_conversion() {
559        // bail! can convert via .into()
560        let result: Result<()> = (|| {
561            let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
562            bail!(io_err);
563        })();
564        assert!(result.is_err());
565        assert!(matches!(result.unwrap_err(), AntQuicError::Io(_)));
566    }
567
568    // ── Error source chain ──
569
570    #[test]
571    fn error_source_chain_transport() {
572        let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR("chain"));
573        let source = std::error::Error::source(&err);
574        assert!(source.is_some());
575    }
576}