Skip to main content

tuwunel_core/error/
mod.rs

1mod err;
2mod log;
3mod panic;
4mod response;
5mod serde;
6
7use std::{
8	any::Any,
9	borrow::Cow,
10	convert::Infallible,
11	sync::{Mutex, PoisonError},
12};
13
14pub use self::{err::visit, log::*};
15use crate::utils::{assert_ref_unwind_safe, assert_send, assert_sync, assert_unwind_safe};
16
17#[derive(thiserror::Error)]
18pub enum Error {
19	#[error("PANIC!")]
20	PanicAny(Mutex<Box<dyn Any + Send>>),
21	#[error("PANIC! {0}")]
22	Panic(&'static str, Mutex<Box<dyn Any + Send + 'static>>),
23
24	// std
25	#[error(transparent)]
26	Fmt(#[from] std::fmt::Error),
27	#[error(transparent)]
28	FromUtf8(#[from] std::string::FromUtf8Error),
29	#[error("I/O error: {0}")]
30	Io(#[from] std::io::Error),
31	#[error(transparent)]
32	ParseFloat(#[from] std::num::ParseFloatError),
33	#[error(transparent)]
34	ParseInt(#[from] std::num::ParseIntError),
35	#[error(transparent)]
36	Std(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
37	#[error(transparent)]
38	SystemTime(#[from] std::time::SystemTimeError),
39	#[error(transparent)]
40	ThreadAccessError(#[from] std::thread::AccessError),
41	#[error(transparent)]
42	TryFromInt(#[from] std::num::TryFromIntError),
43	#[error(transparent)]
44	TryFromSlice(#[from] std::array::TryFromSliceError),
45	#[error(transparent)]
46	Utf8(#[from] std::str::Utf8Error),
47
48	// third-party
49	#[error(transparent)]
50	CapacityError(#[from] arrayvec::CapacityError),
51	#[error(transparent)]
52	CargoToml(#[from] cargo_toml::Error),
53	#[error(transparent)]
54	Clap(#[from] clap::error::Error),
55	#[cfg(unix)]
56	#[error(transparent)]
57	Errno(#[from] nix::errno::Errno),
58	#[error(transparent)]
59	Extension(#[from] axum::extract::rejection::ExtensionRejection),
60	#[error(transparent)]
61	Figment(Box<figment::error::Error>),
62	#[error(transparent)]
63	HtmlFormDe(#[from] serde_html_form::de::Error),
64	#[error(transparent)]
65	HtmlFormSer(#[from] serde_html_form::ser::Error),
66	#[error(transparent)]
67	Http(#[from] http::Error),
68	#[error(transparent)]
69	HttpHeader(#[from] http::header::InvalidHeaderValue),
70	#[error("Join error: {0}")]
71	JoinError(#[from] tokio::task::JoinError),
72	#[error(transparent)]
73	Json(#[from] serde_json::Error),
74	#[error(transparent)]
75	JsParseInt(#[from] ruma::JsParseIntError), // js_int re-export
76	#[error(transparent)]
77	JsTryFromInt(#[from] ruma::JsTryFromIntError), // js_int re-export
78	#[error(transparent)]
79	ObjectStore(#[from] object_store::Error),
80	#[error(transparent)]
81	Path(#[from] axum::extract::rejection::PathRejection),
82	#[error("Mutex poisoned: {0}")]
83	Poison(Cow<'static, str>),
84	#[error("Regex error: {0}")]
85	Regex(#[from] regex::Error),
86	#[error("Request error: {0}")]
87	Reqwest(#[from] reqwest::Error),
88	#[error("{0}")]
89	SerdeDe(Cow<'static, str>),
90	#[error("{0}")]
91	SerdeSer(Cow<'static, str>),
92	#[error(transparent)]
93	TomlDe(#[from] toml::de::Error),
94	#[error(transparent)]
95	TomlSer(#[from] toml::ser::Error),
96	#[error("Tracing filter error: {0}")]
97	TracingFilter(#[from] tracing_subscriber::filter::ParseError),
98	#[error("Tracing reload error: {0}")]
99	TracingReload(#[from] tracing_subscriber::reload::Error),
100	#[error(transparent)]
101	TypedHeader(#[from] axum_extra::typed_header::TypedHeaderRejection),
102	#[error(transparent)]
103	UrlParse(#[from] url::ParseError),
104	#[error(transparent)]
105	Yaml(#[from] serde_yaml::Error),
106
107	// ruma/tuwunel
108	#[error("Arithmetic operation failed: {0}")]
109	Arithmetic(Cow<'static, str>),
110
111	/// State-res `auth_check` rejection sentinel.
112	///
113	/// Surfaces to the wire as 403 / M_FORBIDDEN with the Display text
114	/// `Auth check failed: {inner}`. Exists so callers can pattern-match the
115	/// cause without grepping the message text.
116	#[error("Auth check failed: {0}")]
117	AuthCheck(Box<Self>),
118	#[error("{0}: {1}")]
119	BadRequest(ruma::api::error::ErrorKind, &'static str), //TODO: remove
120	#[error("{0}")]
121	BadServerResponse(Cow<'static, str>),
122	#[error(transparent)]
123	CanonicalJson(#[from] ruma::CanonicalJsonError),
124	#[error("There was a problem with the '{0}' directive in your configuration: {1}")]
125	Config(&'static str, Cow<'static, str>),
126	#[error("{0}")]
127	Conflict(Cow<'static, str>), // This is only needed for when a room alias already exists
128	#[error(transparent)]
129	ContentDisposition(#[from] ruma::http_headers::ContentDispositionParseError),
130	#[error("{0}")]
131	Database(Cow<'static, str>),
132	#[error("Feature '{0}' is not available on this server.")]
133	FeatureDisabled(Cow<'static, str>),
134	#[error("Remote server {0} responded with: {1}")]
135	Federation(ruma::OwnedServerName, ruma::api::error::Error),
136	#[error("{0}: {1:#?}")]
137	HttpJson(http::StatusCode, axum::Json<serde_json::Value>),
138	#[error("{0} in {1}")]
139	InconsistentRoomState(&'static str, ruma::OwnedRoomId),
140	#[error(transparent)]
141	IntoHttp(#[from] ruma::api::error::IntoHttpError),
142	#[error("{0}")]
143	Ldap(Cow<'static, str>),
144	#[error(transparent)]
145	Mxc(#[from] ruma::MxcUriError),
146	#[error(transparent)]
147	Mxid(#[from] ruma::IdParseError),
148	#[error(transparent)]
149	PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError),
150	#[error("from {0}: {1}")]
151	Redaction(ruma::OwnedServerName, ruma::canonical_json::CanonicalJsonFieldError),
152	#[error("{0}: {1}")]
153	Request(ruma::api::error::ErrorKind, Cow<'static, str>, http::StatusCode),
154	#[error(transparent)]
155	Ruma(#[from] ruma::api::error::Error),
156	#[error(transparent)]
157	Signatures(#[from] ruma::signatures::VerificationError),
158	#[error(transparent)]
159	SignaturesJson(#[from] ruma::signatures::JsonError),
160	#[error("uiaa")]
161	Uiaa(ruma::api::client::uiaa::UiaaInfo),
162
163	// unique / untyped
164	#[error("{0}")]
165	Err(Cow<'static, str>),
166}
167
168static _IS_SEND: () = assert_send::<Error>();
169static _IS_SYNC: () = assert_sync::<Error>();
170static _IS_UNWIND_SAFE: () = assert_unwind_safe::<Error>();
171static _IS_REF_UNWIND_SAFE: () = assert_ref_unwind_safe::<Error>();
172
173impl Error {
174	#[inline]
175	#[must_use]
176	pub fn from_errno() -> Self { Self::Io(std::io::Error::last_os_error()) }
177
178	//#[deprecated]
179	pub fn bad_database(message: &'static str) -> Self {
180		crate::err!(Database(error!("{message}")))
181	}
182
183	/// Sanitizes public-facing errors that can leak sensitive information.
184	pub fn sanitized_message(&self) -> String {
185		match self {
186			| Self::Database(..) => String::from("Database error occurred."),
187			| Self::Io(..) => String::from("I/O error occurred."),
188			| _ => self.message(),
189		}
190	}
191
192	/// Generate the error message string.
193	pub fn message(&self) -> String {
194		match self {
195			| Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
196			| Self::Ruma(error) => response::ruma_error_message(error),
197			| _ => format!("{self}"),
198		}
199	}
200
201	/// Returns the Matrix error code / error kind
202	#[inline]
203	pub fn kind(&self) -> ruma::api::error::ErrorKind {
204		use ruma::api::error::{
205			ErrorKind,
206			ErrorKind::{FeatureDisabled, NotJson, Unknown},
207		};
208
209		match self {
210			| Self::FeatureDisabled(..) => FeatureDisabled,
211			| Self::CanonicalJson(..) | Self::Json(..) => NotJson,
212			| Self::AuthCheck(..) => ErrorKind::forbidden(),
213			| Self::BadRequest(kind, ..) | Self::Request(kind, ..) => kind.clone(),
214			| Self::Federation(_, error) | Self::Ruma(error) =>
215				response::ruma_error_kind(error).clone(),
216			| _ => Unknown,
217		}
218	}
219
220	/// Returns the HTTP error code or closest approximation based on error
221	/// variant.
222	pub fn status_code(&self) -> http::StatusCode {
223		use http::StatusCode;
224
225		match self {
226			| Self::AuthCheck(..) => StatusCode::FORBIDDEN,
227			| Self::Conflict(_) => StatusCode::CONFLICT, // room alias exists
228			| Self::Federation(_, error) | Self::Ruma(error) => error.status_code,
229			| Self::FeatureDisabled(..)
230			| Self::CanonicalJson(..)
231			| Self::Json(..)
232			| Self::JsParseInt(..)
233			| Self::JsTryFromInt(..) => response::bad_request_code(&self.kind()),
234			| Self::BadRequest(kind, ..) => response::bad_request_code(kind),
235			| Self::Request(kind, _, code) => response::status_code(kind, *code),
236			| Self::Io(error) => response::io_error_code(error.kind()),
237			| Self::HttpJson(code, ..) => *code,
238			| Self::Reqwest(error) => error
239				.status()
240				.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
241			| _ => StatusCode::INTERNAL_SERVER_ERROR,
242		}
243	}
244
245	/// Returns true for "not found" errors. This means anything that qualifies
246	/// as a "not found" from any variant's contained error type. This call is
247	/// often used as a special case to eliminate a contained Option with a
248	/// Result where Ok(None) is instead Err(e) if e.is_not_found().
249	#[inline]
250	pub fn is_not_found(&self) -> bool { self.status_code() == http::StatusCode::NOT_FOUND }
251}
252
253impl std::fmt::Debug for Error {
254	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255		write!(f, "{}", self.message())
256	}
257}
258
259impl<T> From<PoisonError<T>> for Error {
260	#[cold]
261	#[inline(never)]
262	fn from(e: PoisonError<T>) -> Self { Self::Poison(e.to_string().into()) }
263}
264
265impl From<figment::error::Error> for Error {
266	#[cold]
267	#[inline(never)]
268	fn from(e: figment::error::Error) -> Self { Self::Figment(Box::new(e)) }
269}
270
271#[expect(clippy::fallible_impl_from)]
272impl From<Infallible> for Error {
273	#[cold]
274	#[inline(never)]
275	fn from(_e: Infallible) -> Self {
276		panic!("infallible error should never exist");
277	}
278}
279
280#[cold]
281#[inline(never)]
282pub fn infallible(_e: &Infallible) {
283	panic!("infallible error should never exist");
284}
285
286/// Convenience functor for fundamental Error::sanitized_message(); see member.
287#[inline]
288#[must_use]
289#[expect(clippy::needless_pass_by_value)]
290pub fn sanitized_message(e: Error) -> String { e.sanitized_message() }