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