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 server rejected the connection with an auth status. See [`crate::ConnectError`].
44	#[error(transparent)]
45	Connect(#[from] crate::ConnectError),
46
47	/// Both halves of the QUIC/WebSocket race failed, so neither error alone tells the story.
48	#[cfg(feature = "websocket")]
49	#[error("failed to connect to server: QUIC failed: {quic}; WebSocket failed: {websocket}")]
50	TransportRace {
51		/// Why the QUIC attempt failed.
52		quic: Arc<Error>,
53		/// Why the WebSocket attempt failed.
54		websocket: Arc<Error>,
55	},
56
57	/// An `iroh://` URL was dialed but the client was built without an Iroh endpoint.
58	#[cfg(feature = "iroh")]
59	#[error("Iroh support is not enabled")]
60	IrohDisabled,
61
62	/// A client certificate was configured, but this QUIC backend can't do mTLS.
63	#[error("tls.root (mTLS) is not supported by the selected QUIC backend")]
64	MtlsUnsupported,
65
66	/// The server's WebTransport response carried a status outside the valid HTTP range.
67	#[error("invalid status code")]
68	InvalidStatusCode,
69
70	/// Reconnecting gave up, usually after the backoff timeout expired. The string has the details.
71	#[error("{0}")]
72	Reconnect(String),
73
74	/// Loading certificates or building the TLS config failed.
75	#[error(transparent)]
76	Tls(Arc<crate::tls::Error>),
77
78	/// The Quinn backend failed.
79	#[cfg(feature = "quinn")]
80	#[error(transparent)]
81	Quinn(Arc<crate::quinn::Error>),
82
83	/// The noq backend failed.
84	#[cfg(feature = "noq")]
85	#[error(transparent)]
86	Noq(Arc<crate::noq::Error>),
87
88	/// The quiche backend failed.
89	#[cfg(feature = "quiche")]
90	#[error(transparent)]
91	Quiche(Arc<crate::quiche::Error>),
92
93	/// The Iroh backend failed.
94	#[cfg(feature = "iroh")]
95	#[error(transparent)]
96	Iroh(Arc<crate::iroh::Error>),
97
98	/// The WebSocket fallback transport failed.
99	#[cfg(feature = "websocket")]
100	#[error(transparent)]
101	WebSocket(Arc<crate::websocket::Error>),
102
103	/// The TCP (qmux) transport failed.
104	#[cfg(feature = "tcp")]
105	#[error(transparent)]
106	Tcp(Arc<crate::tcp::Error>),
107
108	/// The Unix socket transport failed.
109	#[cfg(all(feature = "uds", unix))]
110	#[error(transparent)]
111	Unix(Arc<crate::unix::Error>),
112}
113
114impl Error {
115	/// The auth rejection behind this error, digging through backend and race variants.
116	pub fn connect_error(&self) -> Option<crate::ConnectError> {
117		match self {
118			Self::Connect(err) => Some(*err),
119			Self::MoqNet(moq_net::Error::Unauthorized) => Some(crate::ConnectError::Unauthorized),
120			#[cfg(feature = "quinn")]
121			Self::Quinn(err) => err.connect_error(),
122			#[cfg(feature = "noq")]
123			Self::Noq(err) => err.connect_error(),
124			#[cfg(feature = "quiche")]
125			Self::Quiche(err) => err.connect_error(),
126			#[cfg(feature = "websocket")]
127			Self::TransportRace { quic, websocket } => quic.connect_error().or_else(|| websocket.connect_error()),
128			#[cfg(feature = "websocket")]
129			Self::WebSocket(err) => err.connect_error(),
130			_ => None,
131		}
132	}
133
134	/// True if the server rejected us for auth reasons, so retrying won't help without new credentials.
135	pub fn is_auth(&self) -> bool {
136		self.connect_error().is_some_and(|err| err.is_auth())
137	}
138}
139
140// The wrapped sources aren't `Clone`, so `#[from]` can't store them behind `Arc`
141// directly. These hand-written conversions keep `?` ergonomic at the call sites.
142impl From<std::io::Error> for Error {
143	fn from(err: std::io::Error) -> Self {
144		Self::Io(Arc::new(err))
145	}
146}
147
148impl From<tracing_subscriber::filter::ParseError> for Error {
149	fn from(err: tracing_subscriber::filter::ParseError) -> Self {
150		Self::Directive(Arc::new(err))
151	}
152}
153
154impl From<crate::tls::Error> for Error {
155	fn from(err: crate::tls::Error) -> Self {
156		Self::Tls(Arc::new(err))
157	}
158}
159
160#[cfg(feature = "quinn")]
161impl From<crate::quinn::Error> for Error {
162	fn from(err: crate::quinn::Error) -> Self {
163		if let Some(err) = err.connect_error() {
164			return Self::Connect(err);
165		}
166
167		Self::Quinn(Arc::new(err))
168	}
169}
170
171#[cfg(feature = "noq")]
172impl From<crate::noq::Error> for Error {
173	fn from(err: crate::noq::Error) -> Self {
174		if let Some(err) = err.connect_error() {
175			return Self::Connect(err);
176		}
177
178		Self::Noq(Arc::new(err))
179	}
180}
181
182#[cfg(feature = "quiche")]
183impl From<crate::quiche::Error> for Error {
184	fn from(err: crate::quiche::Error) -> Self {
185		if let Some(err) = err.connect_error() {
186			return Self::Connect(err);
187		}
188
189		Self::Quiche(Arc::new(err))
190	}
191}
192
193#[cfg(feature = "iroh")]
194impl From<crate::iroh::Error> for Error {
195	fn from(err: crate::iroh::Error) -> Self {
196		Self::Iroh(Arc::new(err))
197	}
198}
199
200#[cfg(feature = "websocket")]
201impl From<crate::websocket::Error> for Error {
202	fn from(err: crate::websocket::Error) -> Self {
203		if let Some(err) = err.connect_error() {
204			return Self::Connect(err);
205		}
206
207		Self::WebSocket(Arc::new(err))
208	}
209}
210
211#[cfg(feature = "tcp")]
212impl From<crate::tcp::Error> for Error {
213	fn from(err: crate::tcp::Error) -> Self {
214		Self::Tcp(Arc::new(err))
215	}
216}
217
218#[cfg(all(feature = "uds", unix))]
219impl From<crate::unix::Error> for Error {
220	fn from(err: crate::unix::Error) -> Self {
221		Self::Unix(Arc::new(err))
222	}
223}
224
225/// Convenience alias for results produced by this crate.
226pub type Result<T> = std::result::Result<T, Error>;
227
228#[cfg(all(test, feature = "websocket"))]
229mod tests {
230	use super::*;
231
232	#[test]
233	fn transport_race_propagates_nested_connect_errors() {
234		let quic = Error::TransportRace {
235			quic: Arc::new(crate::ConnectError::Unauthorized.into()),
236			websocket: Arc::new(crate::ConnectError::Forbidden.into()),
237		};
238		assert_eq!(quic.connect_error(), Some(crate::ConnectError::Unauthorized));
239
240		let websocket = Error::TransportRace {
241			quic: Arc::new(Error::ConnectFailed),
242			websocket: Arc::new(crate::ConnectError::Forbidden.into()),
243		};
244		assert_eq!(websocket.connect_error(), Some(crate::ConnectError::Forbidden));
245	}
246}