Files
quicproquo/crates/quicprochat-p2p/src/error.rs
Christian Nennemann 024b6c91d1 feat(p2p): add production infrastructure modules
- error.rs: Structured error types with context for all subsystems
  (transport, routing, crypto, protocol, store, config)
- config.rs: Runtime configuration with TOML parsing and validation
- metrics.rs: Counter/gauge/histogram metrics with transport-specific
  tracking and JSON-serializable snapshots
- rate_limit.rs: Token bucket rate limiting with per-peer tracking,
  duty cycle enforcement for LoRa, and backpressure control

These modules provide the foundation for production deployment.
2026-04-01 09:16:44 +02:00

355 lines
9.4 KiB
Rust

//! Production-ready error types for the mesh P2P layer.
//!
//! This module provides structured error types with context for debugging
//! and recovery. Errors are categorized by subsystem for easier handling.
use std::fmt;
use thiserror::Error;
use crate::address::MeshAddress;
use crate::transport::TransportAddr;
/// Top-level mesh error type.
#[derive(Debug, Error)]
pub enum MeshError {
/// Transport layer errors.
#[error("transport error: {0}")]
Transport(#[from] TransportError),
/// Routing errors.
#[error("routing error: {0}")]
Routing(#[from] RoutingError),
/// Crypto/encryption errors.
#[error("crypto error: {0}")]
Crypto(#[from] CryptoError),
/// Protocol errors (malformed messages, version mismatch).
#[error("protocol error: {0}")]
Protocol(#[from] ProtocolError),
/// Store/cache errors.
#[error("store error: {0}")]
Store(#[from] StoreError),
/// Configuration errors.
#[error("config error: {0}")]
Config(#[from] ConfigError),
/// Internal errors (bugs, invariant violations).
#[error("internal error: {0}")]
Internal(String),
}
/// Transport layer errors.
#[derive(Debug, Error)]
pub enum TransportError {
/// Failed to send data.
#[error("send failed to {dest}: {reason}")]
SendFailed { dest: String, reason: String },
/// Failed to receive data.
#[error("receive failed: {0}")]
ReceiveFailed(String),
/// Connection failed or lost.
#[error("connection to {dest} failed: {reason}")]
ConnectionFailed { dest: String, reason: String },
/// Transport not available.
#[error("transport '{name}' not available")]
NotAvailable { name: String },
/// No transports registered.
#[error("no transports registered")]
NoTransports,
/// MTU exceeded.
#[error("payload {size} bytes exceeds MTU {mtu} bytes")]
MtuExceeded { size: usize, mtu: usize },
/// Duty cycle limit reached.
#[error("duty cycle limit reached: {used_ms}ms used of {limit_ms}ms allowed")]
DutyCycleExceeded { used_ms: u64, limit_ms: u64 },
/// Timeout waiting for response.
#[error("timeout waiting for response from {dest}")]
Timeout { dest: String },
/// I/O error.
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
/// Routing errors.
#[derive(Debug, Error)]
pub enum RoutingError {
/// No route to destination.
#[error("no route to {0}")]
NoRoute(String),
/// Route expired.
#[error("route to {dest} expired (last seen {age_secs}s ago)")]
RouteExpired { dest: String, age_secs: u64 },
/// Too many hops.
#[error("max hops ({max}) exceeded for message to {dest}")]
MaxHopsExceeded { dest: String, max: u8 },
/// Message expired.
#[error("message expired (TTL {ttl_secs}s, age {age_secs}s)")]
MessageExpired { ttl_secs: u32, age_secs: u64 },
/// Duplicate message (dedup).
#[error("duplicate message ID {0}")]
Duplicate(String),
/// Routing table full.
#[error("routing table full ({capacity} entries)")]
TableFull { capacity: usize },
}
/// Crypto/encryption errors.
#[derive(Debug, Error)]
pub enum CryptoError {
/// Signature verification failed.
#[error("signature verification failed for {context}")]
SignatureInvalid { context: String },
/// Decryption failed.
#[error("decryption failed: {0}")]
DecryptionFailed(String),
/// Key not found.
#[error("key not found for {0}")]
KeyNotFound(String),
/// KeyPackage invalid or expired.
#[error("KeyPackage invalid: {0}")]
KeyPackageInvalid(String),
/// Replay attack detected.
#[error("replay detected: sequence {seq} already seen from {sender}")]
ReplayDetected { sender: String, seq: u32 },
/// Wrong epoch.
#[error("wrong epoch: expected {expected}, got {got}")]
WrongEpoch { expected: u16, got: u16 },
/// MLS error (from openmls).
#[error("MLS error: {0}")]
Mls(String),
}
/// Protocol errors.
#[derive(Debug, Error)]
pub enum ProtocolError {
/// Unknown message type.
#[error("unknown message type: 0x{0:02x}")]
UnknownMessageType(u8),
/// Invalid message format.
#[error("invalid message format: {0}")]
InvalidFormat(String),
/// Version mismatch.
#[error("protocol version mismatch: expected {expected}, got {got}")]
VersionMismatch { expected: u8, got: u8 },
/// Required field missing.
#[error("required field missing: {0}")]
MissingField(String),
/// CBOR decode error.
#[error("CBOR decode error: {0}")]
CborDecode(String),
/// CBOR encode error.
#[error("CBOR encode error: {0}")]
CborEncode(String),
/// Message too large.
#[error("message too large: {size} bytes (max {max})")]
MessageTooLarge { size: usize, max: usize },
}
/// Store/cache errors.
#[derive(Debug, Error)]
pub enum StoreError {
/// Store is full.
#[error("store full: {current}/{capacity} items")]
Full { current: usize, capacity: usize },
/// Item not found.
#[error("item not found: {0}")]
NotFound(String),
/// Persistence error.
#[error("persistence error: {0}")]
Persistence(String),
/// Serialization error.
#[error("serialization error: {0}")]
Serialization(String),
}
/// Configuration errors.
#[derive(Debug, Error)]
pub enum ConfigError {
/// Invalid configuration value.
#[error("invalid config value for '{key}': {reason}")]
InvalidValue { key: String, reason: String },
/// Missing required configuration.
#[error("missing required config: {0}")]
Missing(String),
/// Configuration parse error.
#[error("config parse error: {0}")]
Parse(String),
}
/// Result type alias for mesh operations.
pub type MeshResult<T> = Result<T, MeshError>;
/// Error context extension trait for adding context to errors.
pub trait ErrorContext<T> {
/// Add context to an error.
fn context(self, context: impl Into<String>) -> MeshResult<T>;
/// Add context with a closure (lazy evaluation).
fn with_context<F>(self, f: F) -> MeshResult<T>
where
F: FnOnce() -> String;
}
impl<T, E: Into<MeshError>> ErrorContext<T> for Result<T, E> {
fn context(self, context: impl Into<String>) -> MeshResult<T> {
self.map_err(|e| {
let err = e.into();
MeshError::Internal(format!("{}: {}", context.into(), err))
})
}
fn with_context<F>(self, f: F) -> MeshResult<T>
where
F: FnOnce() -> String,
{
self.map_err(|e| {
let err = e.into();
MeshError::Internal(format!("{}: {}", f(), err))
})
}
}
/// Convert anyhow errors to MeshError.
impl From<anyhow::Error> for MeshError {
fn from(e: anyhow::Error) -> Self {
MeshError::Internal(e.to_string())
}
}
/// Helper to create transport send errors.
impl TransportError {
pub fn send_failed(dest: &TransportAddr, reason: impl Into<String>) -> Self {
Self::SendFailed {
dest: dest.to_string(),
reason: reason.into(),
}
}
pub fn connection_failed(dest: &TransportAddr, reason: impl Into<String>) -> Self {
Self::ConnectionFailed {
dest: dest.to_string(),
reason: reason.into(),
}
}
}
/// Helper to create routing errors.
impl RoutingError {
pub fn no_route(addr: &MeshAddress) -> Self {
Self::NoRoute(format!("{}", addr))
}
pub fn no_route_bytes(addr: &[u8]) -> Self {
Self::NoRoute(hex::encode(&addr[..8.min(addr.len())]))
}
}
/// Helper to create crypto errors.
impl CryptoError {
pub fn signature_invalid(context: impl Into<String>) -> Self {
Self::SignatureInvalid {
context: context.into(),
}
}
pub fn replay(sender: &MeshAddress, seq: u32) -> Self {
Self::ReplayDetected {
sender: format!("{}", sender),
seq,
}
}
}
/// Helper to create protocol errors.
impl ProtocolError {
pub fn cbor_decode(e: impl fmt::Display) -> Self {
Self::CborDecode(e.to_string())
}
pub fn cbor_encode(e: impl fmt::Display) -> Self {
Self::CborEncode(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display() {
let err = TransportError::SendFailed {
dest: "tcp:127.0.0.1:8080".to_string(),
reason: "connection refused".to_string(),
};
assert!(err.to_string().contains("tcp:127.0.0.1:8080"));
assert!(err.to_string().contains("connection refused"));
}
#[test]
fn error_conversion() {
let transport_err = TransportError::NoTransports;
let mesh_err: MeshError = transport_err.into();
assert!(matches!(mesh_err, MeshError::Transport(_)));
}
#[test]
fn routing_error_helpers() {
let addr = MeshAddress::from_bytes([0xAB; 16]);
let err = RoutingError::no_route(&addr);
assert!(err.to_string().contains("no route"));
}
#[test]
fn crypto_error_helpers() {
let addr = MeshAddress::from_bytes([0xCD; 16]);
let err = CryptoError::replay(&addr, 42);
assert!(err.to_string().contains("42"));
}
#[test]
fn context_extension() {
fn fallible() -> Result<(), TransportError> {
Err(TransportError::NoTransports)
}
let result: MeshResult<()> = fallible().context("during startup");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(err_str.contains("during startup"));
}
}