Skip to main content

moq_native/
error.rs

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