Skip to main content

tuwunel_core/error/
mod.rs

1//! Defines shared error and result types.
2//!
3//! Errors retain protocol and transport context across crate boundaries. The
4//! module also provides the macros used to construct and report them.
5
6mod err;
7mod log;
8mod panic;
9mod response;
10mod serde;
11#[cfg(test)]
12mod tests;
13
14use std::{
15	any::Any,
16	borrow::Cow,
17	convert::Infallible,
18	sync::{Mutex, PoisonError},
19};
20
21pub use self::{err::visit, log::*};
22use crate::utils::{assert_ref_unwind_safe, assert_send, assert_sync, assert_unwind_safe};
23
24/// Unifies failures raised by the core crate.
25///
26/// Variants preserve typed causes where available and carry contextual text for
27/// domain-specific failures. Conversion implementations allow callers to
28/// propagate common dependency errors with `?`.
29#[derive(thiserror::Error)]
30pub enum Error {
31	/// Carries an arbitrary panic payload.
32	///
33	/// The payload is protected by a mutex so the error remains shareable
34	/// across unwind boundaries. Use the panic helpers to resume unwinding or
35	/// inspect it.
36	#[error("PANIC!")]
37	PanicAny(Mutex<Box<dyn Any + Send>>),
38
39	/// Carries a panic payload and its extracted static message.
40	///
41	/// The message supports diagnostics without consuming the payload. The
42	/// mutex keeps the payload available across unwind boundaries.
43	#[error("PANIC! {0}")]
44	Panic(&'static str, Mutex<Box<dyn Any + Send + 'static>>),
45
46	// std
47	/// Reports a formatting failure.
48	///
49	/// Automatic conversion preserves the original source error. Its display
50	/// text is forwarded unchanged.
51	#[error(transparent)]
52	Fmt(#[from] std::fmt::Error),
53
54	/// Reports an invalid UTF-8 byte sequence while constructing a string.
55	///
56	/// Automatic conversion preserves the original bytes and source error. Its
57	/// display text is forwarded unchanged.
58	#[error(transparent)]
59	FromUtf8(#[from] std::string::FromUtf8Error),
60
61	/// Reports an input or output failure.
62	///
63	/// Automatic conversion preserves the original I/O error and its kind. HTTP
64	/// response mapping may use the contained error kind.
65	#[error("I/O error: {0}")]
66	Io(#[from] std::io::Error),
67
68	/// Reports a floating-point parsing failure.
69	///
70	/// Automatic conversion preserves the original source error. Its display
71	/// text is forwarded unchanged.
72	#[error(transparent)]
73	ParseFloat(#[from] std::num::ParseFloatError),
74
75	/// Reports an integer parsing failure.
76	///
77	/// Automatic conversion preserves the original source error. Its display
78	/// text is forwarded unchanged.
79	#[error(transparent)]
80	ParseInt(#[from] std::num::ParseIntError),
81
82	/// Carries a dynamically typed standard error.
83	///
84	/// The boxed source must be safe to send and share between threads. Its
85	/// display text and source chain remain available for diagnostics.
86	#[error(transparent)]
87	Std(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
88
89	/// Reports a system clock value earlier than the requested reference time.
90	///
91	/// Automatic conversion preserves the original source error. Its display
92	/// text is forwarded unchanged.
93	#[error(transparent)]
94	SystemTime(#[from] std::time::SystemTimeError),
95
96	/// Reports failure to access thread-local state.
97	///
98	/// Automatic conversion preserves the original source error. Its display
99	/// text is forwarded unchanged.
100	#[error(transparent)]
101	ThreadAccessError(#[from] std::thread::AccessError),
102
103	/// Reports an integer conversion outside the destination range.
104	///
105	/// Automatic conversion preserves the original source error. Its display
106	/// text is forwarded unchanged.
107	#[error(transparent)]
108	TryFromInt(#[from] std::num::TryFromIntError),
109
110	/// Reports conversion from a slice with an incompatible length.
111	///
112	/// Automatic conversion preserves the original source error. Its display
113	/// text is forwarded unchanged.
114	#[error(transparent)]
115	TryFromSlice(#[from] std::array::TryFromSliceError),
116
117	/// Reports an invalid borrowed UTF-8 byte sequence.
118	///
119	/// Automatic conversion preserves the original source error. Its display
120	/// text is forwarded unchanged.
121	#[error(transparent)]
122	Utf8(#[from] std::str::Utf8Error),
123
124	// third-party
125	/// Reports that a fixed-capacity collection cannot accept another item.
126	///
127	/// Automatic conversion preserves the original capacity error. Its display
128	/// text is forwarded unchanged.
129	#[error(transparent)]
130	CapacityError(#[from] arrayvec::CapacityError),
131
132	/// Reports failure to parse Cargo manifest data.
133	///
134	/// Automatic conversion preserves the original source error. Its display
135	/// text is forwarded unchanged.
136	#[error(transparent)]
137	CargoToml(#[from] cargo_toml::Error),
138
139	/// Reports a command-line parsing or presentation failure.
140	///
141	/// Automatic conversion preserves the original Clap error. Its display text
142	/// is forwarded unchanged.
143	#[error(transparent)]
144	Clap(#[from] clap::error::Error),
145
146	/// Reports a Unix system error number.
147	///
148	/// Automatic conversion preserves the original error number. Its display
149	/// text is forwarded unchanged.
150	#[cfg(unix)]
151	#[error(transparent)]
152	Errno(#[from] nix::errno::Errno),
153
154	/// Reports rejection of a required Axum request extension.
155	///
156	/// Automatic conversion preserves the extractor rejection. Its display text
157	/// is forwarded unchanged.
158	#[error(transparent)]
159	Extension(#[from] axum::extract::rejection::ExtensionRejection),
160
161	/// Reports a configuration extraction failure.
162	///
163	/// The boxed Figment error preserves its complete source and diagnostic
164	/// context. A dedicated conversion boxes it for ordinary `?` propagation.
165	#[error(transparent)]
166	Figment(Box<figment::error::Error>),
167
168	/// Reports failure to deserialize an HTML form.
169	///
170	/// Automatic conversion preserves the original source error. Its display
171	/// text is forwarded unchanged.
172	#[error(transparent)]
173	HtmlFormDe(#[from] serde_html_form::de::Error),
174
175	/// Reports failure to serialize an HTML form.
176	///
177	/// Automatic conversion preserves the original source error. Its display
178	/// text is forwarded unchanged.
179	#[error(transparent)]
180	HtmlFormSer(#[from] serde_html_form::ser::Error),
181
182	/// Reports failure to construct an HTTP value.
183	///
184	/// Automatic conversion preserves the original source error. Its display
185	/// text is forwarded unchanged.
186	#[error(transparent)]
187	Http(#[from] http::Error),
188
189	/// Reports an invalid HTTP header value.
190	///
191	/// Automatic conversion preserves the original source error. Its display
192	/// text is forwarded unchanged.
193	#[error(transparent)]
194	HttpHeader(#[from] http::header::InvalidHeaderValue),
195
196	/// Reports failure of a spawned asynchronous task.
197	///
198	/// The Tokio join error records cancellation and panic state. Panic helpers
199	/// can recover its payload when the task panicked.
200	#[error("Join error: {0}")]
201	JoinError(#[from] tokio::task::JoinError),
202
203	/// Reports failure to serialize or deserialize JSON.
204	///
205	/// Automatic conversion preserves the original source error. Matrix error
206	/// mapping classifies this variant as invalid JSON.
207	#[error(transparent)]
208	Json(#[from] serde_json::Error),
209
210	/// Reports failure to parse a Matrix-compatible JavaScript integer.
211	///
212	/// Automatic conversion preserves the re-exported integer error. Matrix
213	/// response mapping treats it as a bad request.
214	#[error(transparent)]
215	JsParseInt(#[from] ruma::JsParseIntError), // js_int re-export
216
217	/// Reports a value outside the Matrix JavaScript-integer range.
218	///
219	/// Automatic conversion preserves the re-exported conversion error. Matrix
220	/// response mapping treats it as a bad request.
221	#[error(transparent)]
222	JsTryFromInt(#[from] ruma::JsTryFromIntError), // js_int re-export
223
224	/// Reports an object-storage operation failure.
225	///
226	/// Automatic conversion preserves the backend source error. Its display
227	/// text is forwarded unchanged.
228	#[error(transparent)]
229	ObjectStore(#[from] object_store::Error),
230
231	/// Reports rejection of an Axum path parameter.
232	///
233	/// Automatic conversion preserves the extractor rejection. Its display text
234	/// is forwarded unchanged.
235	#[error(transparent)]
236	Path(#[from] axum::extract::rejection::PathRejection),
237
238	/// Reports access to a poisoned synchronization primitive.
239	///
240	/// The stored text reports the poisoning without retaining the guard.
241	/// Poison conversion supplies the originating error text.
242	#[error("Mutex poisoned: {0}")]
243	Poison(Cow<'static, str>),
244
245	/// Reports an invalid regular expression.
246	///
247	/// Automatic conversion preserves the original source error. The formatted
248	/// message identifies the regex failure.
249	#[error("Regex error: {0}")]
250	Regex(#[from] regex::Error),
251
252	/// Reports an HTTP client request failure.
253	///
254	/// Automatic conversion preserves status and transport details from
255	/// Reqwest. HTTP response mapping reuses its status when one is available.
256	#[error("Request error: {0}")]
257	Reqwest(#[from] reqwest::Error),
258
259	/// Reports a custom deserialization failure.
260	///
261	/// The message may borrow static text or own formatted context. It is
262	/// exposed directly as the error display.
263	#[error("{0}")]
264	SerdeDe(Cow<'static, str>),
265
266	/// Reports a custom serialization failure.
267	///
268	/// The message may borrow static text or own formatted context. It is
269	/// exposed directly as the error display.
270	#[error("{0}")]
271	SerdeSer(Cow<'static, str>),
272
273	/// Reports failure to deserialize TOML.
274	///
275	/// Automatic conversion preserves the original source error. Its display
276	/// text is forwarded unchanged.
277	#[error(transparent)]
278	TomlDe(#[from] toml::de::Error),
279
280	/// Reports failure to serialize TOML.
281	///
282	/// Automatic conversion preserves the original source error. Its display
283	/// text is forwarded unchanged.
284	#[error(transparent)]
285	TomlSer(#[from] toml::ser::Error),
286
287	/// Reports an invalid tracing filter directive.
288	///
289	/// Automatic conversion preserves the filter parser's source error. The
290	/// formatted message identifies the tracing subsystem.
291	#[error("Tracing filter error: {0}")]
292	TracingFilter(#[from] tracing_subscriber::filter::ParseError),
293
294	/// Reports failure to reload a tracing layer.
295	///
296	/// Automatic conversion preserves the reload source error. The formatted
297	/// message identifies the tracing subsystem.
298	#[error("Tracing reload error: {0}")]
299	TracingReload(#[from] tracing_subscriber::reload::Error),
300
301	/// Reports rejection of a typed HTTP header.
302	///
303	/// Automatic conversion preserves the extractor rejection. Its display text
304	/// is forwarded unchanged.
305	#[error(transparent)]
306	TypedHeader(#[from] axum_extra::typed_header::TypedHeaderRejection),
307
308	/// Reports failure to parse a URL.
309	///
310	/// Automatic conversion preserves the original source error. Its display
311	/// text is forwarded unchanged.
312	#[error(transparent)]
313	UrlParse(#[from] url::ParseError),
314
315	/// Reports failure to serialize or deserialize YAML.
316	///
317	/// Automatic conversion preserves the original source error. Its display
318	/// text is forwarded unchanged.
319	#[error(transparent)]
320	Yaml(#[from] serde_yaml::Error),
321
322	// ruma/tuwunel
323	/// Reports an arithmetic operation that cannot produce a valid result.
324	///
325	/// The message records contextual conversion, range, overflow, and
326	/// underflow failures. It can supplement lower-level typed numeric source
327	/// errors.
328	#[error("Arithmetic operation failed: {0}")]
329	Arithmetic(Cow<'static, str>),
330
331	/// State-res `auth_check` rejection sentinel.
332	///
333	/// Surfaces to the wire as 403 / M_FORBIDDEN with the Display text
334	/// `Auth check failed: {inner}`. Exists so callers can pattern-match the
335	/// cause without grepping the message text.
336	#[error("Auth check failed: {0}")]
337	AuthCheck(Box<Self>),
338
339	/// Reports a legacy structured Matrix request error.
340	///
341	/// The variant pairs a Matrix error kind with static public text. Response
342	/// mapping derives the appropriate HTTP status from that kind.
343	#[error("{0}: {1}")]
344	BadRequest(ruma::api::error::ErrorKind, &'static str), //TODO: remove
345
346	/// Reports an invalid or unusable response from a remote server.
347	///
348	/// The message carries protocol context suitable for diagnostics. No typed
349	/// remote response is retained.
350	#[error("{0}")]
351	BadServerResponse(Cow<'static, str>),
352
353	/// Reports invalid canonical JSON.
354	///
355	/// Automatic conversion preserves the original canonicalization error.
356	/// Matrix error mapping classifies this variant as invalid JSON.
357	#[error(transparent)]
358	CanonicalJson(#[from] ruma::CanonicalJsonError),
359
360	/// Reports an invalid configuration directive.
361	///
362	/// The static directive name identifies the setting and the accompanying
363	/// message explains why its value cannot be used.
364	#[error("There was a problem with the '{0}' directive in your configuration: {1}")]
365	Config(&'static str, Cow<'static, str>),
366
367	/// Reports a resource conflict.
368	///
369	/// This variant currently represents an already occupied room alias. HTTP
370	/// response mapping emits a conflict status.
371	#[error("{0}")]
372	Conflict(Cow<'static, str>), // This is only needed for when a room alias already exists
373
374	/// Reports an invalid Matrix content-disposition header.
375	///
376	/// Automatic conversion preserves the original parser error. Its display
377	/// text is forwarded unchanged.
378	#[error(transparent)]
379	ContentDisposition(#[from] ruma::http_headers::ContentDispositionParseError),
380
381	/// Reports a database operation or invariant failure.
382	///
383	/// The message carries storage context without exposing it in sanitized
384	/// client-facing output. Database failures default to an internal status.
385	#[error("{0}")]
386	Database(Cow<'static, str>),
387
388	/// Reports use of a feature disabled by server configuration.
389	///
390	/// The stored feature name is included in the public message. Matrix
391	/// response mapping assigns the matching feature-disabled error kind.
392	#[error("Feature '{0}' is not available on this server.")]
393	FeatureDisabled(Cow<'static, str>),
394
395	/// Reports an error response received from a federated server.
396	///
397	/// The variant preserves both the origin and its structured Matrix error,
398	/// so internal callers can dispatch on what the remote said. Response
399	/// mapping does not forward that status and kind unconditionally.
400	#[error("Remote server {0} responded with: {1}")]
401	Federation(ruma::OwnedServerName, ruma::api::error::Error),
402
403	/// Carries a preconstructed HTTP status and JSON response body.
404	///
405	/// Response mapping preserves the caller-selected status. The formatted
406	/// JSON becomes the message of a standard Matrix error response.
407	#[error("{0}: {1:#?}")]
408	HttpJson(http::StatusCode, axum::Json<serde_json::Value>),
409
410	/// Reports an invariant violation in a room's persisted state.
411	///
412	/// The static message names the failed invariant and the room identifier
413	/// locates the affected state.
414	#[error("{0} in {1}")]
415	InconsistentRoomState(&'static str, ruma::OwnedRoomId),
416
417	/// Reports failure to convert a Matrix response into HTTP form.
418	///
419	/// Automatic conversion preserves the original Ruma source error. Its
420	/// display text is forwarded unchanged.
421	#[error(transparent)]
422	IntoHttp(#[from] ruma::api::error::IntoHttpError),
423
424	/// Reports an LDAP operation failure.
425	///
426	/// The message records directory-service context not represented by a
427	/// common typed source. Response mapping treats it as an internal error.
428	#[error("{0}")]
429	Ldap(Cow<'static, str>),
430
431	/// Reports an invalid Matrix content URI.
432	///
433	/// Automatic conversion preserves the original URI error. Its display text
434	/// is forwarded unchanged.
435	#[error(transparent)]
436	Mxc(#[from] ruma::MxcUriError),
437
438	/// Reports an invalid Matrix identifier.
439	///
440	/// Automatic conversion preserves the original identifier parser error. Its
441	/// display text is forwarded unchanged.
442	#[error(transparent)]
443	Mxid(#[from] ruma::IdParseError),
444
445	/// Reports invalid room power-level content or arithmetic.
446	///
447	/// Automatic conversion preserves the original Ruma power-level error. Its
448	/// display text is forwarded unchanged.
449	#[error(transparent)]
450	PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError),
451
452	/// Reports failure to redact canonical JSON from a remote server.
453	///
454	/// The variant records the origin alongside the invalid canonical field.
455	/// The formatted message keeps both pieces of context.
456	#[error("from {0}: {1}")]
457	Redaction(ruma::OwnedServerName, ruma::canonical_json::CanonicalJsonFieldError),
458
459	/// Carries a structured Matrix client error response.
460	///
461	/// The variant stores the Matrix error kind, public message, and preferred
462	/// HTTP status. Response mapping may refine the status from the kind.
463	#[error("{0}: {1}")]
464	Request(ruma::api::error::ErrorKind, Cow<'static, str>, http::StatusCode),
465
466	/// Reports a structured Matrix API error.
467	///
468	/// Automatic conversion preserves the Ruma status, kind, and message.
469	/// Response mapping forwards those structured fields.
470	#[error(transparent)]
471	Ruma(#[from] ruma::api::error::Error),
472
473	/// Reports a Matrix signature verification failure.
474	///
475	/// Automatic conversion preserves the original verification error. Its
476	/// display text is forwarded unchanged.
477	#[error(transparent)]
478	Signatures(#[from] ruma::signatures::VerificationError),
479
480	/// Reports invalid JSON encountered during signature processing.
481	///
482	/// Automatic conversion preserves the signature library's JSON error. Its
483	/// display text is forwarded unchanged.
484	#[error(transparent)]
485	SignaturesJson(#[from] ruma::signatures::JsonError),
486
487	/// Requests an interactive-authentication challenge response.
488	///
489	/// The contained UIAA information is serialized for the client rather than
490	/// treated as an opaque internal failure.
491	#[error("uiaa")]
492	Uiaa(ruma::api::client::uiaa::UiaaInfo),
493
494	// unique / untyped
495	/// Reports an untyped core failure.
496	///
497	/// The message may borrow static text or own formatted context. This
498	/// fallback is used when no structured variant represents the failure.
499	#[error("{0}")]
500	Err(Cow<'static, str>),
501}
502
503static _IS_SEND: () = assert_send::<Error>();
504static _IS_SYNC: () = assert_sync::<Error>();
505static _IS_UNWIND_SAFE: () = assert_unwind_safe::<Error>();
506static _IS_REF_UNWIND_SAFE: () = assert_ref_unwind_safe::<Error>();
507
508impl Error {
509	/// Captures the operating system's most recent error for the current
510	/// thread.
511	///
512	/// The error is sampled when this function is called and wrapped as
513	/// [`Error::Io`]. Platform-specific code remains available through the
514	/// source.
515	#[inline]
516	#[must_use]
517	pub fn from_errno() -> Self { Self::Io(std::io::Error::last_os_error()) }
518
519	/// Constructs a database error from static diagnostic text.
520	///
521	/// The error helper records the call site while preserving the supplied
522	/// message. Callers exposing it publicly can use
523	/// [`Error::sanitized_message`].
524	//#[deprecated]
525	pub fn bad_database(message: &'static str) -> Self {
526		crate::err!(Database(error!("{message}")))
527	}
528
529	/// Produces an error message safe for public responses.
530	///
531	/// Database and I/O details are replaced with generic text to avoid leaking
532	/// sensitive context. Other variants retain their normal message.
533	pub fn sanitized_message(&self) -> String {
534		match self {
535			| Self::Database(..) => String::from("Database error occurred."),
536			| Self::Io(..) => String::from("I/O error occurred."),
537			| _ => self.message(),
538		}
539	}
540
541	/// Formats the diagnostic message for this error.
542	///
543	/// Federation errors include their origin and Ruma errors use their Matrix
544	/// response message. Other variants use their
545	/// [`Display`](std::fmt::Display) implementation.
546	pub fn message(&self) -> String {
547		match self {
548			| Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
549			| Self::Ruma(error) => response::ruma_error_message(error),
550			| _ => format!("{self}"),
551		}
552	}
553
554	/// Returns the Matrix error kind represented by this error.
555	///
556	/// Structured request and federation variants preserve their supplied kind.
557	/// Unclassified internal errors map to `M_UNKNOWN`.
558	#[inline]
559	pub fn kind(&self) -> ruma::api::error::ErrorKind {
560		use ruma::api::error::{
561			ErrorKind,
562			ErrorKind::{FeatureDisabled, NotJson, Unknown},
563		};
564
565		match self {
566			| Self::FeatureDisabled(..) => FeatureDisabled,
567			| Self::CanonicalJson(..) | Self::Json(..) => NotJson,
568			| Self::AuthCheck(..) => ErrorKind::forbidden(),
569			| Self::BadRequest(kind, ..) | Self::Request(kind, ..) => kind.clone(),
570			| Self::Federation(_, error) | Self::Ruma(error) =>
571				response::ruma_error_kind(error).clone(),
572			| _ => Unknown,
573		}
574	}
575
576	/// Returns the HTTP status represented by this error.
577	///
578	/// Structured variants preserve or derive their protocol status, while I/O
579	/// and client errors use their available status metadata. Unclassified
580	/// failures map to an internal-server-error status.
581	pub fn status_code(&self) -> http::StatusCode {
582		use http::StatusCode;
583
584		match self {
585			| Self::AuthCheck(..) => StatusCode::FORBIDDEN,
586			| Self::Conflict(_) => StatusCode::CONFLICT, // room alias exists
587			| Self::Federation(_, error) | Self::Ruma(error) => error.status_code,
588			| Self::FeatureDisabled(..)
589			| Self::CanonicalJson(..)
590			| Self::Json(..)
591			| Self::JsParseInt(..)
592			| Self::JsTryFromInt(..) => response::bad_request_code(&self.kind()),
593			| Self::BadRequest(kind, ..) => response::bad_request_code(kind),
594			| Self::Request(kind, _, code) => response::status_code(kind, *code),
595			| Self::Io(error) => response::io_error_code(error.kind()),
596			| Self::HttpJson(code, ..) => *code,
597			| Self::Reqwest(error) => error
598				.status()
599				.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
600			| _ => StatusCode::INTERNAL_SERVER_ERROR,
601		}
602	}
603
604	/// Tests whether this error maps to an HTTP not-found status.
605	///
606	/// The test includes contained error types whose status mapping yields 404.
607	/// Callers can use it to treat `Err` as the absent case in place of a
608	/// nested `Option`.
609	#[inline]
610	pub fn is_not_found(&self) -> bool { self.status_code() == http::StatusCode::NOT_FOUND }
611}
612
613impl std::fmt::Debug for Error {
614	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615		write!(f, "{}", self.message())
616	}
617}
618
619impl<T> From<PoisonError<T>> for Error {
620	#[cold]
621	#[inline(never)]
622	fn from(e: PoisonError<T>) -> Self { Self::Poison(e.to_string().into()) }
623}
624
625impl From<figment::error::Error> for Error {
626	#[cold]
627	#[inline(never)]
628	fn from(e: figment::error::Error) -> Self { Self::Figment(Box::new(e)) }
629}
630
631#[expect(clippy::fallible_impl_from)]
632impl From<Infallible> for Error {
633	#[cold]
634	#[inline(never)]
635	fn from(_e: Infallible) -> Self {
636		panic!("infallible error should never exist");
637	}
638}
639
640/// Marks an impossible [`Infallible`] error path.
641///
642/// The argument cannot be constructed in safe code, so reaching this function
643/// indicates a violated invariant.
644///
645/// # Panics
646///
647/// Always panics because an `Infallible` error cannot legitimately exist.
648#[cold]
649#[inline(never)]
650pub fn infallible(_e: &Infallible) {
651	panic!("infallible error should never exist");
652}
653
654/// Produces a public-safe message from an owned error.
655///
656/// Its by-value signature adapts [`Error::sanitized_message`] for iterator and
657/// future combinators that consume their item. Sanitization behavior is
658/// identical to the method.
659#[inline]
660#[must_use]
661#[expect(clippy::needless_pass_by_value)]
662pub fn sanitized_message(e: Error) -> String { e.sanitized_message() }