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}