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(#[from] 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	#[error("{0}: {1}")]
111	BadRequest(ruma::api::error::ErrorKind, &'static str), //TODO: remove
112	#[error("{0}")]
113	BadServerResponse(Cow<'static, str>),
114	#[error(transparent)]
115	CanonicalJson(#[from] ruma::CanonicalJsonError),
116	#[error("There was a problem with the '{0}' directive in your configuration: {1}")]
117	Config(&'static str, Cow<'static, str>),
118	#[error("{0}")]
119	Conflict(Cow<'static, str>), // This is only needed for when a room alias already exists
120	#[error(transparent)]
121	ContentDisposition(#[from] ruma::http_headers::ContentDispositionParseError),
122	#[error("{0}")]
123	Database(Cow<'static, str>),
124	#[error("Feature '{0}' is not available on this server.")]
125	FeatureDisabled(Cow<'static, str>),
126	#[error("Remote server {0} responded with: {1}")]
127	Federation(ruma::OwnedServerName, ruma::api::error::Error),
128	#[error("{0}: {1:#?}")]
129	HttpJson(http::StatusCode, axum::Json<serde_json::Value>),
130	#[error("{0} in {1}")]
131	InconsistentRoomState(&'static str, ruma::OwnedRoomId),
132	#[error(transparent)]
133	IntoHttp(#[from] ruma::api::error::IntoHttpError),
134	#[error("{0}")]
135	Ldap(Cow<'static, str>),
136	#[error(transparent)]
137	Mxc(#[from] ruma::MxcUriError),
138	#[error(transparent)]
139	Mxid(#[from] ruma::IdParseError),
140	#[error(transparent)]
141	PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError),
142	#[error("from {0}: {1}")]
143	Redaction(ruma::OwnedServerName, ruma::canonical_json::CanonicalJsonFieldError),
144	#[error("{0}: {1}")]
145	Request(ruma::api::error::ErrorKind, Cow<'static, str>, http::StatusCode),
146	#[error(transparent)]
147	Ruma(#[from] ruma::api::error::Error),
148	#[error(transparent)]
149	Signatures(#[from] ruma::signatures::VerificationError),
150	#[error(transparent)]
151	SignaturesJson(#[from] ruma::signatures::JsonError),
152	#[error("uiaa")]
153	Uiaa(ruma::api::client::uiaa::UiaaInfo),
154
155	// unique / untyped
156	#[error("{0}")]
157	Err(Cow<'static, str>),
158}
159
160static _IS_SEND: () = assert_send::<Error>();
161static _IS_SYNC: () = assert_sync::<Error>();
162static _IS_UNWIND_SAFE: () = assert_unwind_safe::<Error>();
163static _IS_REF_UNWIND_SAFE: () = assert_ref_unwind_safe::<Error>();
164
165impl Error {
166	#[inline]
167	#[must_use]
168	pub fn from_errno() -> Self { Self::Io(std::io::Error::last_os_error()) }
169
170	//#[deprecated]
171	pub fn bad_database(message: &'static str) -> Self {
172		crate::err!(Database(error!("{message}")))
173	}
174
175	/// Sanitizes public-facing errors that can leak sensitive information.
176	pub fn sanitized_message(&self) -> String {
177		match self {
178			| Self::Database(..) => String::from("Database error occurred."),
179			| Self::Io(..) => String::from("I/O error occurred."),
180			| _ => self.message(),
181		}
182	}
183
184	/// Generate the error message string.
185	pub fn message(&self) -> String {
186		match self {
187			| Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
188			| Self::Ruma(error) => response::ruma_error_message(error),
189			| _ => format!("{self}"),
190		}
191	}
192
193	/// Returns the Matrix error code / error kind
194	#[inline]
195	pub fn kind(&self) -> ruma::api::error::ErrorKind {
196		use ruma::api::error::ErrorKind::{FeatureDisabled, NotJson, Unknown};
197
198		match self {
199			| Self::FeatureDisabled(..) => FeatureDisabled,
200			| Self::CanonicalJson(..) | Self::Json(..) => NotJson,
201			| Self::BadRequest(kind, ..) | Self::Request(kind, ..) => kind.clone(),
202			| Self::Federation(_, error) | Self::Ruma(error) =>
203				response::ruma_error_kind(error).clone(),
204			| _ => Unknown,
205		}
206	}
207
208	/// Returns the HTTP error code or closest approximation based on error
209	/// variant.
210	pub fn status_code(&self) -> http::StatusCode {
211		use http::StatusCode;
212
213		match self {
214			| Self::Conflict(_) => StatusCode::CONFLICT, // room alias exists
215			| Self::Federation(_, error) | Self::Ruma(error) => error.status_code,
216			| Self::FeatureDisabled(..)
217			| Self::CanonicalJson(..)
218			| Self::Json(..)
219			| Self::JsParseInt(..)
220			| Self::JsTryFromInt(..) => response::bad_request_code(&self.kind()),
221			| Self::BadRequest(kind, ..) => response::bad_request_code(kind),
222			| Self::Request(kind, _, code) => response::status_code(kind, *code),
223			| Self::Io(error) => response::io_error_code(error.kind()),
224			| Self::HttpJson(code, ..) => *code,
225			| Self::Reqwest(error) => error
226				.status()
227				.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
228			| _ => StatusCode::INTERNAL_SERVER_ERROR,
229		}
230	}
231
232	/// Returns true for "not found" errors. This means anything that qualifies
233	/// as a "not found" from any variant's contained error type. This call is
234	/// often used as a special case to eliminate a contained Option with a
235	/// Result where Ok(None) is instead Err(e) if e.is_not_found().
236	#[inline]
237	pub fn is_not_found(&self) -> bool { self.status_code() == http::StatusCode::NOT_FOUND }
238}
239
240impl std::fmt::Debug for Error {
241	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242		write!(f, "{}", self.message())
243	}
244}
245
246impl<T> From<PoisonError<T>> for Error {
247	#[cold]
248	#[inline(never)]
249	fn from(e: PoisonError<T>) -> Self { Self::Poison(e.to_string().into()) }
250}
251
252#[expect(clippy::fallible_impl_from)]
253impl From<Infallible> for Error {
254	#[cold]
255	#[inline(never)]
256	fn from(_e: Infallible) -> Self {
257		panic!("infallible error should never exist");
258	}
259}
260
261#[cold]
262#[inline(never)]
263pub fn infallible(_e: &Infallible) {
264	panic!("infallible error should never exist");
265}
266
267/// Convenience functor for fundamental Error::sanitized_message(); see member.
268#[inline]
269#[must_use]
270#[expect(clippy::needless_pass_by_value)]
271pub fn sanitized_message(e: Error) -> String { e.sanitized_message() }