quic_reverse/
error.rs

1// Copyright 2024-2026 Farlight Networks, LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Error types for quic-reverse sessions.
16
17use quic_reverse_control::RejectCode;
18use thiserror::Error;
19
20/// Errors that can occur during session operations.
21#[derive(Debug, Error)]
22pub enum Error {
23    /// Negotiation failed during session startup.
24    #[error("negotiation failed: {0}")]
25    NegotiationFailed(#[from] NegotiationError),
26
27    /// Protocol violation detected.
28    #[error("protocol violation: {0}")]
29    ProtocolViolation(String),
30
31    /// Operation timed out.
32    #[error("timeout: {0}")]
33    Timeout(TimeoutKind),
34
35    /// Transport-level error.
36    #[error("transport error: {0}")]
37    Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
38
39    /// Stream open request was rejected by the peer.
40    #[error("stream rejected: {code}{}", reason.as_ref().map(|r| format!(": {r}")).unwrap_or_default())]
41    StreamRejected {
42        /// The rejection code.
43        code: RejectCode,
44        /// Optional reason message.
45        reason: Option<String>,
46    },
47
48    /// Session has been closed.
49    #[error("session closed")]
50    SessionClosed,
51
52    /// Connection to peer was lost.
53    #[error("disconnected")]
54    Disconnected,
55
56    /// Capacity limit reached.
57    #[error("capacity limit reached: {0}")]
58    CapacityExceeded(&'static str),
59
60    /// Configuration error.
61    #[error("configuration error: {0}")]
62    Config(#[from] crate::config::ConfigError),
63
64    /// Control protocol error.
65    #[error("control protocol error: {0}")]
66    Control(#[from] quic_reverse_control::ControlError),
67
68    /// Codec error during message encoding/decoding.
69    #[error("codec error: {0}")]
70    Codec(#[from] quic_reverse_control::CodecError),
71}
72
73impl Error {
74    /// Creates a transport error from any error type.
75    pub fn transport<E>(error: E) -> Self
76    where
77        E: std::error::Error + Send + Sync + 'static,
78    {
79        Self::Transport(Box::new(error))
80    }
81
82    /// Creates a protocol violation error.
83    pub fn protocol_violation(message: impl Into<String>) -> Self {
84        Self::ProtocolViolation(message.into())
85    }
86
87    /// Creates a stream rejected error.
88    #[must_use]
89    pub const fn stream_rejected(code: RejectCode, reason: Option<String>) -> Self {
90        Self::StreamRejected { code, reason }
91    }
92}
93
94/// Errors that can occur during negotiation.
95#[derive(Debug, Error)]
96pub enum NegotiationError {
97    /// No compatible protocol version found.
98    #[error("no compatible protocol version: local supports {local:?}, remote supports {remote}")]
99    VersionMismatch {
100        /// Versions supported locally.
101        local: Vec<u16>,
102        /// Version offered by remote.
103        remote: u16,
104    },
105
106    /// Required feature not supported by peer.
107    #[error("required feature not supported: {0}")]
108    MissingFeature(String),
109
110    /// Unexpected message received during negotiation.
111    #[error("unexpected message during negotiation")]
112    UnexpectedMessage,
113
114    /// Negotiation timed out.
115    #[error("negotiation timed out")]
116    Timeout,
117}
118
119/// Types of timeout that can occur.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum TimeoutKind {
122    /// Timeout waiting for negotiation to complete.
123    Negotiation,
124    /// Timeout waiting for an `OpenResponse`.
125    OpenRequest,
126    /// Timeout waiting for a stream to be bound after acceptance.
127    StreamBind,
128    /// Timeout waiting for a ping response.
129    Ping,
130}
131
132impl std::fmt::Display for TimeoutKind {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        match self {
135            Self::Negotiation => write!(f, "negotiation"),
136            Self::OpenRequest => write!(f, "open request"),
137            Self::StreamBind => write!(f, "stream bind"),
138            Self::Ping => write!(f, "ping"),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn error_display() {
149        let err = Error::stream_rejected(RejectCode::Unauthorized, Some("denied".into()));
150        assert!(err.to_string().contains("unauthorized"));
151        assert!(err.to_string().contains("denied"));
152    }
153
154    #[test]
155    fn timeout_kind_display() {
156        assert_eq!(TimeoutKind::Negotiation.to_string(), "negotiation");
157        assert_eq!(TimeoutKind::OpenRequest.to_string(), "open request");
158    }
159
160    #[test]
161    fn negotiation_error_display() {
162        let err = NegotiationError::VersionMismatch {
163            local: vec![1, 2],
164            remote: 3,
165        };
166        let msg = err.to_string();
167        assert!(msg.contains("[1, 2]"));
168        assert!(msg.contains("3"));
169    }
170}