Skip to main content

moq_transport/session/
error.rs

1use crate::{coding, serve, setup};
2
3#[derive(thiserror::Error, Debug, Clone)]
4pub enum SessionError {
5    #[error("webtransport error: {0}")]
6    WebTransport(#[from] web_transport::Error),
7
8    #[error("encode error: {0}")]
9    Encode(#[from] coding::EncodeError),
10
11    #[error("decode error: {0}")]
12    Decode(#[from] coding::DecodeError),
13
14    // TODO move to a ConnectError
15    #[error("unsupported versions: client={0:?} server={1:?}")]
16    Version(setup::Versions, setup::Versions),
17
18    /// TODO SLG - eventually remove or morph into error for incorrect control message for publisher/subscriber
19    /// The role negiotiated in the handshake was violated. For example, a publisher sent a SUBSCRIBE, or a subscriber sent an OBJECT.
20    #[error("role violation")]
21    RoleViolation,
22
23    /// Some VarInt was too large and we were too lazy to handle it
24    #[error("varint bounds exceeded")]
25    BoundsExceeded(#[from] coding::BoundsExceeded),
26
27    /// A duplicate ID was used
28    #[error("duplicate")]
29    Duplicate,
30
31    #[error("internal error")]
32    Internal,
33
34    #[error("serve error: {0}")]
35    Serve(#[from] serve::ServeError),
36
37    #[error("wrong size")]
38    WrongSize,
39}
40
41// Session Termination Error Codes from draft-ietf-moq-transport-14 Section 13.1.1
42impl SessionError {
43    /// An integer code that is sent over the wire.
44    /// Returns Session Termination Error Codes per draft-14.
45    pub fn code(&self) -> u64 {
46        match self {
47            // PROTOCOL_VIOLATION (0x3) - The role negotiated in the handshake was violated
48            Self::RoleViolation => 0x3,
49            // INTERNAL_ERROR (0x1) - Generic internal errors
50            Self::WebTransport(_) => 0x1,
51            Self::Encode(_) => 0x1,
52            Self::BoundsExceeded(_) => 0x1,
53            Self::Internal => 0x1,
54            // VERSION_NEGOTIATION_FAILED (0x15)
55            Self::Version(..) => 0x15,
56            // PROTOCOL_VIOLATION (0x3) - Malformed messages
57            Self::Decode(_) => 0x3,
58            Self::WrongSize => 0x3,
59            // DUPLICATE_TRACK_ALIAS (0x5)
60            Self::Duplicate => 0x5,
61            // Delegate to ServeError for per-request error codes
62            Self::Serve(err) => err.code(),
63        }
64    }
65
66    /// Helper for unimplemented protocol features
67    /// Logs a warning and returns a NotImplemented error instead of panicking
68    pub fn unimplemented(feature: &str) -> Self {
69        Self::Serve(serve::ServeError::not_implemented_ctx(feature))
70    }
71
72    /// Returns true if this error represents a graceful connection close.
73    ///
74    /// For WebTransport, a graceful close is a `CLOSE_WEBTRANSPORT_SESSION` capsule
75    /// with code 0. For raw QUIC, it's `APPLICATION_CLOSE` with code 0 (NO_ERROR).
76    /// Both are normal session termination, not error conditions.
77    ///
78    /// This method checks for:
79    /// - WebTransport `Closed(0, _)` — web-transport-quinn v0.11+ typically converts
80    ///   HTTP/3-encoded `ApplicationClosed` codes into `WebTransportError::Closed(code, reason)`
81    ///   during `SessionError` conversion when decoding via `error_from_http3` succeeds
82    /// - Raw QUIC `ApplicationClosed` with code 0
83    /// - The local side closing the connection (`LocallyClosed`)
84    ///
85    /// ## Implementation Notes
86    ///
87    /// We pattern match on `web_transport_quinn::SessionError` variants. In v0.11+,
88    /// WebTransport graceful closes arrive as `WebTransportError::Closed(0, _)` because
89    /// the crate decodes HTTP/3 error codes at the `SessionError` level. For raw QUIC
90    /// connections, the close code is checked directly on `ConnectionError::ApplicationClosed`.
91    ///
92    /// **Coupling note**: This implementation is coupled to `web-transport-quinn` and
93    /// `quinn`. When transitioning to a different WebTransport backend (e.g., tokio-quiche),
94    /// ensure the replacement provides equivalent error introspection, or update this
95    /// method to handle the new error types.
96    pub fn is_graceful_close(&self) -> bool {
97        match self {
98            Self::WebTransport(wt_err) => match wt_err {
99                web_transport::Error::Session(session_err) => {
100                    is_session_error_graceful(session_err)
101                }
102                web_transport::Error::Read(read_err) => {
103                    if let web_transport::quinn::ReadError::SessionError(session_err) = read_err {
104                        return is_session_error_graceful(session_err);
105                    }
106                    false
107                }
108                web_transport::Error::Write(write_err) => {
109                    if let web_transport::quinn::WriteError::SessionError(session_err) = write_err {
110                        return is_session_error_graceful(session_err);
111                    }
112                    false
113                }
114                _ => false,
115            },
116            _ => false,
117        }
118    }
119}
120
121impl From<SessionError> for serve::ServeError {
122    fn from(err: SessionError) -> Self {
123        match err {
124            SessionError::Serve(err) => err,
125            _ => serve::ServeError::internal_ctx(format!("session error: {}", err)),
126        }
127    }
128}
129
130/// Helper to check if a `web_transport_quinn::SessionError` represents a graceful close.
131///
132/// This handles:
133/// - WebTransport connections: `WebTransportError::Closed(0, _)` — web-transport-quinn v0.11+
134///   typically decodes HTTP/3-encoded close codes at this layer (when `SessionError` conversion
135///   applies), so graceful closes usually arrive here rather than as a raw
136///   `ConnectionError::ApplicationClosed`.
137/// - Raw QUIC connections: `ConnectionError::ApplicationClosed` with code 0
138/// - Local close: `ConnectionError::LocallyClosed`
139fn is_session_error_graceful(err: &web_transport::quinn::SessionError) -> bool {
140    use web_transport::quinn::{SessionError, WebTransportError};
141
142    match err {
143        SessionError::ConnectionError(conn_err) => is_connection_error_graceful(conn_err),
144        // WebTransport graceful close: peer sent close with code 0
145        SessionError::WebTransportError(WebTransportError::Closed(0, _)) => true,
146        // Other WebTransport errors (UnknownSession, read/write errors, non-zero close codes)
147        SessionError::WebTransportError(_) => false,
148        // SendDatagramError doesn't represent connection close
149        SessionError::SendDatagramError(_) => false,
150    }
151}
152
153/// Helper to check if a `quinn::ConnectionError` represents a graceful close.
154///
155/// Note: In web-transport-quinn v0.11+, WebTransport `ApplicationClosed` with an HTTP/3-encoded
156/// close code is usually converted to `WebTransportError::Closed` during `SessionError` conversion
157/// when decoding succeeds. This function primarily handles raw QUIC (moqt:// ALPN) connections
158/// or non-decodable cases where the close code is not HTTP/3 encoded.
159fn is_connection_error_graceful(err: &web_transport::quinn::quinn::ConnectionError) -> bool {
160    use web_transport::quinn::quinn::ConnectionError;
161
162    match err {
163        ConnectionError::ApplicationClosed(close) => {
164            let code = close.error_code.into_inner();
165
166            // Check for raw QUIC code 0 (direct MoQ-over-QUIC)
167            if code == 0 {
168                return true;
169            }
170
171            // Check for WebTransport code 0 (HTTP/3 encoded)
172            // This is a fallback — in v0.11+, WebTransport closes are typically caught
173            // by is_session_error_graceful's WebTransportError::Closed branch.
174            if let Some(wt_code) = web_transport::quinn::proto::error_from_http3(code) {
175                return wt_code == 0;
176            }
177
178            false
179        }
180        // LocallyClosed means we closed the connection ourselves
181        ConnectionError::LocallyClosed => true,
182        // Other errors are not graceful closes
183        _ => false,
184    }
185}