Skip to main content

moq_native/
error.rs

1use std::sync::Arc;
2
3/// Errors produced while configuring or establishing native MoQ connections.
4///
5/// Backend-specific failures live in per-backend error types ([`crate::tls::Error`],
6/// the per-backend `Error` types, etc.). They're wrapped in `Arc` here so the aggregate
7/// stays `Clone` even though the underlying transport/IO errors are not.
8#[derive(Debug, Clone, thiserror::Error)]
9#[non_exhaustive]
10pub enum Error {
11	/// Reading or writing a socket, certificate, or key file failed.
12	#[error(transparent)]
13	Io(Arc<std::io::Error>),
14
15	/// The MoQ session itself failed, after the transport was established.
16	#[error(transparent)]
17	MoqNet(#[from] moq_net::Error),
18
19	/// The log filter string (ex. `RUST_LOG`) isn't a valid tracing directive.
20	#[error("invalid log directive")]
21	Directive(#[source] Arc<tracing_subscriber::filter::ParseError>),
22
23	/// Logging was initialized twice, or something else already claimed the global subscriber.
24	#[error("failed to set global tracing subscriber")]
25	SetSubscriber(#[source] Arc<tracing_subscriber::util::TryInitError>),
26
27	/// Logging couldn't attach to Android's logcat.
28	#[error("failed to initialize Android logcat layer")]
29	Logcat(#[source] Arc<std::io::Error>),
30
31	/// No backend feature is compiled in that can serve this URL. The string names the features to enable.
32	#[error("{0}")]
33	NoBackend(&'static str),
34
35	/// A qlog directory was configured but this build can't capture traces.
36	#[error("qlog capture requires the 'qlog' feature")]
37	QlogUnsupported,
38
39	/// Every backend we tried gave up without reporting why.
40	#[error("failed to connect to server")]
41	ConnectFailed,
42
43	/// The dial and handshake together outlived the connect timeout.
44	///
45	/// Not every transport bounds its own dial: QUIC gives up on its own, but a peer
46	/// that completes the TCP handshake and then never speaks leaves the WebSocket
47	/// fallback (and the MoQ handshake that follows either transport) pending with
48	/// nothing to time it out. This deadline turns that into an error the caller can
49	/// retry instead of a wait that never ends.
50	#[error("connect timed out after {0:?}")]
51	ConnectTimeout(std::time::Duration),
52
53	/// The server rejected the connection with an auth status. See [`crate::ConnectError`].
54	#[error(transparent)]
55	Connect(#[from] crate::ConnectError),
56
57	/// Both halves of the QUIC/WebSocket race failed, so neither error alone tells the story.
58	#[cfg(feature = "websocket")]
59	#[error("failed to connect to server: QUIC failed: {quic}; WebSocket failed: {websocket}")]
60	TransportRace {
61		/// Why the QUIC attempt failed.
62		quic: Arc<Error>,
63		/// Why the WebSocket attempt failed.
64		websocket: Arc<Error>,
65	},
66
67	/// An `iroh://` URL was dialed but the client was built without an Iroh endpoint.
68	#[cfg(feature = "iroh")]
69	#[error("Iroh support is not enabled")]
70	IrohDisabled,
71
72	/// A client certificate was configured, but this QUIC backend can't do mTLS.
73	#[error("tls.root (mTLS) is not supported by the selected QUIC backend")]
74	MtlsUnsupported,
75
76	/// The server's WebTransport response carried a status outside the valid HTTP range.
77	#[error("invalid status code")]
78	InvalidStatusCode,
79
80	/// Reconnecting gave up, usually after the backoff timeout expired. The string has the details.
81	#[error("{0}")]
82	Reconnect(String),
83
84	/// Loading certificates or building the TLS config failed.
85	#[error(transparent)]
86	Tls(Arc<crate::tls::Error>),
87
88	/// The Quinn backend failed.
89	#[cfg(feature = "quinn")]
90	#[error(transparent)]
91	Quinn(Arc<crate::quinn::Error>),
92
93	/// The noq backend failed.
94	#[cfg(feature = "noq")]
95	#[error(transparent)]
96	Noq(Arc<crate::noq::Error>),
97
98	/// The quiche backend failed.
99	#[cfg(feature = "quiche")]
100	#[error(transparent)]
101	Quiche(Arc<crate::quiche::Error>),
102
103	/// The Iroh backend failed.
104	#[cfg(feature = "iroh")]
105	#[error(transparent)]
106	Iroh(Arc<crate::iroh::Error>),
107
108	/// The WebSocket fallback transport failed.
109	#[cfg(feature = "websocket")]
110	#[error(transparent)]
111	WebSocket(Arc<crate::websocket::Error>),
112
113	/// The TCP (qmux) transport failed.
114	#[cfg(feature = "tcp")]
115	#[error(transparent)]
116	Tcp(Arc<crate::tcp::Error>),
117
118	/// The Unix socket transport failed.
119	#[cfg(all(feature = "uds", unix))]
120	#[error(transparent)]
121	Unix(Arc<crate::unix::Error>),
122}
123
124impl Error {
125	/// The auth rejection behind this error, digging through backend and race variants.
126	pub fn connect_error(&self) -> Option<crate::ConnectError> {
127		match self {
128			Self::Connect(err) => Some(*err),
129			Self::MoqNet(moq_net::Error::Unauthorized) => Some(crate::ConnectError::Unauthorized),
130			#[cfg(feature = "quinn")]
131			Self::Quinn(err) => err.connect_error(),
132			#[cfg(feature = "noq")]
133			Self::Noq(err) => err.connect_error(),
134			#[cfg(feature = "quiche")]
135			Self::Quiche(err) => err.connect_error(),
136			#[cfg(feature = "websocket")]
137			Self::TransportRace { quic, websocket } => quic.connect_error().or_else(|| websocket.connect_error()),
138			#[cfg(feature = "websocket")]
139			Self::WebSocket(err) => err.connect_error(),
140			_ => None,
141		}
142	}
143
144	/// True if the server rejected us for auth reasons, so retrying won't help without new credentials.
145	pub fn is_auth(&self) -> bool {
146		self.connect_error().is_some_and(|err| err.is_auth())
147	}
148}
149
150// The wrapped sources aren't `Clone`, so `#[from]` can't store them behind `Arc`
151// directly. These hand-written conversions keep `?` ergonomic at the call sites.
152impl From<std::io::Error> for Error {
153	fn from(err: std::io::Error) -> Self {
154		Self::Io(Arc::new(err))
155	}
156}
157
158impl From<tracing_subscriber::filter::ParseError> for Error {
159	fn from(err: tracing_subscriber::filter::ParseError) -> Self {
160		Self::Directive(Arc::new(err))
161	}
162}
163
164impl From<crate::tls::Error> for Error {
165	fn from(err: crate::tls::Error) -> Self {
166		Self::Tls(Arc::new(err))
167	}
168}
169
170#[cfg(feature = "quinn")]
171impl From<crate::quinn::Error> for Error {
172	fn from(err: crate::quinn::Error) -> Self {
173		if let Some(err) = err.connect_error() {
174			return Self::Connect(err);
175		}
176
177		Self::Quinn(Arc::new(err))
178	}
179}
180
181#[cfg(feature = "noq")]
182impl From<crate::noq::Error> for Error {
183	fn from(err: crate::noq::Error) -> Self {
184		if let Some(err) = err.connect_error() {
185			return Self::Connect(err);
186		}
187
188		Self::Noq(Arc::new(err))
189	}
190}
191
192#[cfg(feature = "quiche")]
193impl From<crate::quiche::Error> for Error {
194	fn from(err: crate::quiche::Error) -> Self {
195		if let Some(err) = err.connect_error() {
196			return Self::Connect(err);
197		}
198
199		Self::Quiche(Arc::new(err))
200	}
201}
202
203#[cfg(feature = "iroh")]
204impl From<crate::iroh::Error> for Error {
205	fn from(err: crate::iroh::Error) -> Self {
206		Self::Iroh(Arc::new(err))
207	}
208}
209
210#[cfg(feature = "websocket")]
211impl From<crate::websocket::Error> for Error {
212	fn from(err: crate::websocket::Error) -> Self {
213		if let Some(err) = err.connect_error() {
214			return Self::Connect(err);
215		}
216
217		Self::WebSocket(Arc::new(err))
218	}
219}
220
221#[cfg(feature = "tcp")]
222impl From<crate::tcp::Error> for Error {
223	fn from(err: crate::tcp::Error) -> Self {
224		Self::Tcp(Arc::new(err))
225	}
226}
227
228#[cfg(all(feature = "uds", unix))]
229impl From<crate::unix::Error> for Error {
230	fn from(err: crate::unix::Error) -> Self {
231		Self::Unix(Arc::new(err))
232	}
233}
234
235/// Convenience alias for results produced by this crate.
236pub type Result<T> = std::result::Result<T, Error>;
237
238#[cfg(all(test, feature = "websocket"))]
239mod tests {
240	use super::*;
241
242	#[test]
243	fn transport_race_propagates_nested_connect_errors() {
244		let quic = Error::TransportRace {
245			quic: Arc::new(crate::ConnectError::Unauthorized.into()),
246			websocket: Arc::new(crate::ConnectError::Forbidden.into()),
247		};
248		assert_eq!(quic.connect_error(), Some(crate::ConnectError::Unauthorized));
249
250		let websocket = Error::TransportRace {
251			quic: Arc::new(Error::ConnectFailed),
252			websocket: Arc::new(crate::ConnectError::Forbidden.into()),
253		};
254		assert_eq!(websocket.connect_error(), Some(crate::ConnectError::Forbidden));
255	}
256}