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	/// The idle timeout is longer than QUIC's millisecond varint can carry.
49	#[error("idle timeout must be under 2^62 milliseconds")]
50	IdleTimeoutRange,
51
52	/// The backoff would retry with no delay at all, spinning instead of pacing.
53	#[error("backoff initial, multiplier, and max must all be non-zero, or reconnecting spins")]
54	BackoffUnpaced,
55
56	/// Every backend we tried gave up without reporting why.
57	#[error("failed to connect to server")]
58	ConnectFailed,
59
60	/// The dial and handshake together outlived the connect timeout.
61	///
62	/// Not every transport bounds its own dial: QUIC gives up on its own, but a peer
63	/// that completes the TCP handshake and then never speaks leaves the WebSocket
64	/// fallback (and the MoQ handshake that follows either transport) pending with
65	/// nothing to time it out. This deadline turns that into an error the caller can
66	/// retry instead of a wait that never ends.
67	#[error("connect timed out after {0:?}")]
68	ConnectTimeout(std::time::Duration),
69
70	/// The server rejected the connection with an auth status. See [`crate::ConnectError`].
71	#[error(transparent)]
72	Connect(#[from] crate::ConnectError),
73
74	/// Both halves of the QUIC/WebSocket race failed, so neither error alone tells the story.
75	#[cfg(feature = "websocket")]
76	#[error("failed to connect to server: QUIC failed: {quic}; WebSocket failed: {websocket}")]
77	TransportRace {
78		/// Why the QUIC attempt failed.
79		quic: Arc<Error>,
80		/// Why the WebSocket attempt failed.
81		websocket: Arc<Error>,
82	},
83
84	/// An `iroh://` URL was dialed but the client was built without an Iroh endpoint.
85	#[cfg(feature = "iroh")]
86	#[error("Iroh support is not enabled")]
87	IrohDisabled,
88
89	/// A client certificate was configured, but this QUIC backend can't do mTLS.
90	#[error("tls.root (mTLS) is not supported by the selected QUIC backend")]
91	MtlsUnsupported,
92
93	/// The server's WebTransport response carried a status outside the valid HTTP range.
94	#[error("invalid status code")]
95	InvalidStatusCode,
96
97	/// Reconnecting gave up, usually after the backoff timeout expired. The string has the details.
98	#[error("{0}")]
99	Reconnect(String),
100
101	/// Loading certificates or building the TLS config failed.
102	#[error(transparent)]
103	Tls(Arc<crate::tls::Error>),
104
105	/// The Quinn backend failed.
106	#[cfg(feature = "quinn")]
107	#[error(transparent)]
108	Quinn(Arc<crate::quinn::Error>),
109
110	/// The noq backend failed.
111	#[cfg(feature = "noq")]
112	#[error(transparent)]
113	Noq(Arc<crate::noq::Error>),
114
115	/// The quiche backend failed.
116	#[cfg(feature = "quiche")]
117	#[error(transparent)]
118	Quiche(Arc<crate::quiche::Error>),
119
120	/// The Iroh backend failed.
121	#[cfg(feature = "iroh")]
122	#[error(transparent)]
123	Iroh(Arc<crate::iroh::Error>),
124
125	/// The WebSocket fallback transport failed.
126	#[cfg(feature = "websocket")]
127	#[error(transparent)]
128	WebSocket(Arc<crate::websocket::Error>),
129
130	/// The TCP (qmux) transport failed.
131	#[cfg(feature = "tcp")]
132	#[error(transparent)]
133	Tcp(Arc<crate::tcp::Error>),
134
135	/// The Unix socket transport failed.
136	#[cfg(all(feature = "uds", unix))]
137	#[error(transparent)]
138	Unix(Arc<crate::unix::Error>),
139}
140
141impl Error {
142	/// The auth rejection behind this error, digging through backend and race variants.
143	pub fn connect_error(&self) -> Option<crate::ConnectError> {
144		match self {
145			Self::Connect(err) => Some(*err),
146			Self::MoqNet(moq_net::Error::Unauthorized) => Some(crate::ConnectError::Unauthorized),
147			#[cfg(feature = "quinn")]
148			Self::Quinn(err) => err.connect_error(),
149			#[cfg(feature = "noq")]
150			Self::Noq(err) => err.connect_error(),
151			#[cfg(feature = "quiche")]
152			Self::Quiche(err) => err.connect_error(),
153			#[cfg(feature = "websocket")]
154			Self::TransportRace { quic, websocket } => quic.connect_error().or_else(|| websocket.connect_error()),
155			#[cfg(feature = "websocket")]
156			Self::WebSocket(err) => err.connect_error(),
157			_ => None,
158		}
159	}
160
161	/// True if the server rejected us for auth reasons, so retrying won't help without new credentials.
162	pub fn is_auth(&self) -> bool {
163		self.connect_error().is_some_and(|err| err.is_auth())
164	}
165
166	/// The HTTP status a server answered a connection attempt with, if it answered with one at all.
167	///
168	/// `None` covers everything else: a dial that never got a response, a QUIC handshake that
169	/// failed, a URL we couldn't parse. Only a status the peer actually sent shows up here, and
170	/// whether it invites another attempt is the caller's call (`408`, `429`, `502`, `503`, and
171	/// `504` are the ones worth repeating). This deliberately does not try to say whether some
172	/// *other* kind of failure is worth retrying; that's a guess, and a backoff budget bounds it
173	/// instead.
174	pub fn status(&self) -> Option<u16> {
175		match self {
176			// A race is only settled when both halves were answered, and answered with something not
177			// worth repeating: one transport being refused says nothing about the other, so a `404`
178			// over QUIC alongside a dead WebSocket is still just a failed dial.
179			#[cfg(feature = "websocket")]
180			Self::TransportRace { quic, websocket } => match (quic.status(), websocket.status()) {
181				(Some(quic), Some(websocket)) if !status_retryable(quic) && !status_retryable(websocket) => Some(quic),
182				_ => None,
183			},
184
185			#[cfg(feature = "quinn")]
186			Self::Quinn(err) => err.status(),
187			#[cfg(feature = "noq")]
188			Self::Noq(err) => err.status(),
189			#[cfg(feature = "quiche")]
190			Self::Quiche(err) => err.status(),
191			#[cfg(feature = "websocket")]
192			Self::WebSocket(err) => err.status(),
193			_ => None,
194		}
195	}
196}
197
198// The wrapped sources aren't `Clone`, so `#[from]` can't store them behind `Arc`
199// directly. These hand-written conversions keep `?` ergonomic at the call sites.
200impl From<std::io::Error> for Error {
201	fn from(err: std::io::Error) -> Self {
202		Self::Io(Arc::new(err))
203	}
204}
205
206impl From<tracing_subscriber::filter::ParseError> for Error {
207	fn from(err: tracing_subscriber::filter::ParseError) -> Self {
208		Self::Directive(Arc::new(err))
209	}
210}
211
212impl From<crate::tls::Error> for Error {
213	fn from(err: crate::tls::Error) -> Self {
214		Self::Tls(Arc::new(err))
215	}
216}
217
218#[cfg(feature = "quinn")]
219impl From<crate::quinn::Error> for Error {
220	fn from(err: crate::quinn::Error) -> Self {
221		if let Some(err) = err.connect_error() {
222			return Self::Connect(err);
223		}
224
225		Self::Quinn(Arc::new(err))
226	}
227}
228
229#[cfg(feature = "noq")]
230impl From<crate::noq::Error> for Error {
231	fn from(err: crate::noq::Error) -> Self {
232		if let Some(err) = err.connect_error() {
233			return Self::Connect(err);
234		}
235
236		Self::Noq(Arc::new(err))
237	}
238}
239
240#[cfg(feature = "quiche")]
241impl From<crate::quiche::Error> for Error {
242	fn from(err: crate::quiche::Error) -> Self {
243		if let Some(err) = err.connect_error() {
244			return Self::Connect(err);
245		}
246
247		Self::Quiche(Arc::new(err))
248	}
249}
250
251#[cfg(feature = "iroh")]
252impl From<crate::iroh::Error> for Error {
253	fn from(err: crate::iroh::Error) -> Self {
254		Self::Iroh(Arc::new(err))
255	}
256}
257
258#[cfg(feature = "websocket")]
259impl From<crate::websocket::Error> for Error {
260	fn from(err: crate::websocket::Error) -> Self {
261		if let Some(err) = err.connect_error() {
262			return Self::Connect(err);
263		}
264
265		Self::WebSocket(Arc::new(err))
266	}
267}
268
269#[cfg(feature = "tcp")]
270impl From<crate::tcp::Error> for Error {
271	fn from(err: crate::tcp::Error) -> Self {
272		Self::Tcp(Arc::new(err))
273	}
274}
275
276#[cfg(all(feature = "uds", unix))]
277impl From<crate::unix::Error> for Error {
278	fn from(err: crate::unix::Error) -> Self {
279		Self::Unix(Arc::new(err))
280	}
281}
282
283/// Convenience alias for results produced by this crate.
284pub type Result<T> = std::result::Result<T, Error>;
285
286#[cfg(all(test, feature = "websocket"))]
287mod tests {
288	use super::*;
289
290	#[test]
291	fn transport_race_propagates_nested_connect_errors() {
292		let quic = Error::TransportRace {
293			quic: Arc::new(crate::ConnectError::Unauthorized.into()),
294			websocket: Arc::new(crate::ConnectError::Forbidden.into()),
295		};
296		assert_eq!(quic.connect_error(), Some(crate::ConnectError::Unauthorized));
297
298		let websocket = Error::TransportRace {
299			quic: Arc::new(Error::ConnectFailed),
300			websocket: Arc::new(crate::ConnectError::Forbidden.into()),
301		};
302		assert_eq!(websocket.connect_error(), Some(crate::ConnectError::Forbidden));
303	}
304}