Skip to main content

arminius_lib_component/
error.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Easy error handling types for the Matrix API
6//!
7//! Contains the trait MatrixApiError, and a couple of error types that are
8//! shared across a lot of endpoints. Every error that can be returned to the
9//! user should implement [MatrixApiError], and
10//! [`axum::response::IntoResponse`], preferably through the derive macro
11//! [`MatrixApiError`](derive@MatrixApiError)
12//!
13//! The point of MatrixApiError, in contrast to other error types in Arminius,
14//! is to handle errors from which no recovery is possible. This can happen
15//! either because it's a "this should never happen bug", like an invalid
16//! database state, or because the error is due to user input.
17//!
18//! From this it follows that the only job of a MatrixApiError is to return
19//! information to the user, and not to provide diagnostics. Diagnostics should,
20//! if needed, be emitted in the [`From<T>`] trait for the relevant error type,
21//! because that is the point at which we give up on handling the error, and
22//! begin propagating it.
23
24mod tests;
25
26use std::panic::Location;
27
28/// Re-export the MatrixApiError derive macro here, so one just needs to depend
29/// on arminius-lib-component when implementing MatrixApiErrors.
30pub use arminius_lib_macros::__MatrixApiError as MatrixApiError;
31use axum::http::StatusCode;
32use ruma::api::error::{
33	Error as MatrixError, ErrorBody as MatrixErrorBody, ErrorKind, StandardErrorBody,
34};
35use serde::{Deserialize, Serialize};
36use serde_json::json;
37
38/// Trait for errors that can be converted into matrix errors
39pub trait MatrixApiError {
40	/// Create a matrix error from the error
41	fn create_matrix_error(&self) -> MatrixError;
42}
43
44/// Create a matrix error
45#[must_use]
46pub fn create_matrix_error(status_code: StatusCode, kind: &ErrorKind, error: &str) -> MatrixError {
47	MatrixErrorBody::Standard(StandardErrorBody::new(kind.clone(), error.to_owned()))
48		.into_error(status_code)
49}
50
51/// Create a custom matrix error
52#[must_use]
53pub fn create_custom_matrix_error(
54	status_code: StatusCode,
55	errcode: &str,
56	error: &str,
57) -> MatrixError {
58	MatrixErrorBody::Json(json!({
59		"errcode": errcode,
60		"error": error,
61	}))
62	.into_error(status_code)
63}
64
65/// Error for when user sends us invalid JSON
66#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
67#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::NotJson, component_crate_root = crate)]
68#[error("{0}")]
69pub struct NotJson(pub String);
70
71/// Error when user JSON is missing fields
72#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
73#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::BadJson, component_crate_root = crate)]
74#[error("{0}")]
75pub struct BadJson(pub String);
76
77/// Error type that may occur when working with user-provided JSON.
78///
79/// This error type figures out on its own whether the error is due to invalid
80/// JSON or JSON not matching the object we're trying to deserialize.
81/// It will then use the underlying serde error to produce a nice error message,
82/// which points out exactly what in the user's JSON string was invalid.
83///
84/// The main point of this type is that it provides [`From<serde_json::Error>`].
85/// It should be used in a handler like this:
86///
87/// ```rust
88/// # use arminius_lib_component::error::InputJsonError;
89/// # use arminius_lib_component::ruma_requests::ArminiusResponse;
90/// # use ruma::api::client::config::set_room_account_data;
91/// # use ruma::events::AnyRoomAccountDataEventContent;
92/// # use ruma::serde::Raw;
93/// async fn room_account_data_set(
94/// 	account_data: Raw<AnyRoomAccountDataEventContent>,
95/// ) -> Result<
96/// 	ArminiusResponse<set_room_account_data::v3::Response>,
97/// 	InputJsonError,
98/// > {
99/// 	let value: serde_json::Value =
100/// 		serde_json::to_value(account_data.into_json())
101/// 			.map_err(InputJsonError::from)?;
102///
103/// 	/* Save the user data somehow */
104///
105/// 	Ok(set_room_account_data::v3::Response::new().into())
106/// }
107/// ```
108///
109/// <div class="example-wrap" style="display:inline-block"><pre
110/// class="compile_fail" style="white-space:normal;font:inherit;">
111///
112/// **Warning**: This error type is meant to simplify the error handling for
113/// <b>user input</b>, provided as part of the current request. If you use it
114/// with data that was not provided by the user as part of this request, you
115/// will risk returning "Bad Request" when the request is perfectly fine.
116///
117/// This means that you should not use this error type when processing
118/// information that you have loaded from the database. If the database has
119/// inconsistent data, that does not mean that the user has made a bad request.
120///
121/// You also risk creating security issues, by leaking information to the user
122/// about data they would not have access to otherwise as part of the error.
123///
124/// </pre></div>
125#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
126#[matrix_api_error(component_crate_root = crate)]
127pub enum InputJsonError {
128	/// Error in JSON parsing, due to an IO error. This is not the users fault,
129	/// so they don't get to hear about it.
130	#[error(transparent)]
131	InternalServerError(InternalServerError),
132	/// The data the user provided is not correct JSON
133	#[error(transparent)]
134	NotJson(NotJson),
135	/// The JSON that the user provided cannot be deserialized into the
136	/// requested datatype
137	#[error(transparent)]
138	BadJson(BadJson),
139}
140
141impl From<serde_json::Error> for InputJsonError {
142	fn from(value: serde_json::Error) -> Self {
143		match value.classify() {
144			serde_json::error::Category::Io => {
145				InputJsonError::InternalServerError(InternalServerError)
146			}
147			serde_json::error::Category::Data => {
148				InputJsonError::BadJson(BadJson(value.to_string()))
149			}
150			serde_json::error::Category::Eof | serde_json::error::Category::Syntax => {
151				InputJsonError::NotJson(NotJson(value.to_string()))
152			}
153		}
154	}
155}
156
157#[expect(
158	missing_docs,
159	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
160)]
161#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
162#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound, component_crate_root = crate)]
163#[error("Event not found")]
164pub struct EventNotFound;
165
166#[expect(
167	missing_docs,
168	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
169)]
170#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
171#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound, component_crate_root = crate)]
172#[error("Room not found")]
173pub struct RoomNotFound;
174
175#[expect(
176	missing_docs,
177	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
178)]
179#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
180#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::Unauthorized, component_crate_root = crate)]
181#[error("Unauthorized")]
182pub struct Unauthorized;
183
184#[expect(
185	missing_docs,
186	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
187)]
188#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
189#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden, component_crate_root = crate)]
190#[error("Forbidden")]
191pub struct Forbidden;
192
193/// A feature that has not been implemented.
194///
195/// While HTTP has the `501 NOT IMPLEMENTED` return code, the Matrix spec
196/// specifically decided not to use it.
197#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize, Eq, PartialEq)]
198#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidParam, component_crate_root = crate)]
199#[error("Feature not implemented: {0}")]
200pub struct FeatureNotImplemented(pub String);
201
202impl FeatureNotImplemented {
203	/// Throws `FeatureNotImplemented` if the `is_used` boolean is true
204	/// saying that `feature` is not implemented yet.
205	///
206	/// # Examples
207	/// This will throw an error if the `ruma_request.event_type` is **not**
208	/// `MessageLikeEventType::RoomMessage`:
209	/// ```ignore
210	/// FeatureNotImplemented::feature_is_used(
211	/// 	ruma_request.event_type != MessageLikeEventType::RoomMessage,
212	/// 	"Non-message room events".to_owned(),
213	/// )?
214	/// ```
215	pub fn feature_is_used(is_used: bool, feature: String) -> Result<(), Self> {
216		if is_used { Err(Self(feature)) } else { Ok(()) }
217	}
218}
219
220/// Represents an unrecoverable error
221///
222/// When an error is encountered in the program, that we are unable to handle,
223/// use `unrecoverable_error` to convert it into an [`InternalServerError`].
224/// This way the error message gets logged, and the correct API response gets
225/// returned to the user.
226#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize, PartialEq, Eq)]
227#[matrix_api_error(status_code = StatusCode::INTERNAL_SERVER_ERROR, error_kind = ErrorKind::Unknown, component_crate_root = crate)]
228#[error("Internal server error")]
229#[non_exhaustive]
230pub struct InternalServerError;
231
232impl InternalServerError {
233	/// Allows creating an internal server error, which does not get logged
234	///
235	/// Useful if implementing a traced From handler, which uses
236	/// InternalServerError as an in-between step.
237	#[must_use]
238	pub fn untraced_internal_server_error() -> Self {
239		InternalServerError
240	}
241}
242
243impl From<arminius_lib_db::DatabaseError> for InternalServerError {
244	#[track_caller]
245	fn from(err: arminius_lib_db::DatabaseError) -> Self {
246		let location = Location::caller();
247		tracing::error!(?err, ?location, "Error communicating with database");
248		InternalServerError
249	}
250}
251
252impl From<arminius_lib_nats::NatsError> for InternalServerError {
253	#[track_caller]
254	fn from(err: arminius_lib_nats::NatsError) -> Self {
255		let location = Location::caller();
256		tracing::error!(?err, ?location, "Error performing NATS call");
257		InternalServerError
258	}
259}
260
261/// Extension trait for [`Result`] that allows us to handle unrecoverable errors
262pub trait ResultExt<T> {
263	/// Equivalent to `map_err`, but always maps to [`InternalServerError`],
264	/// logging the error and context
265	fn unrecoverable_error(self, context: &str) -> Result<T, InternalServerError>;
266}
267
268// The E bound may look weird, and needs a potentially unnecessary allocation,
269// but it accepts `thiserror` and `color_eyre` Errors.
270impl<'a, T, E: Into<Box<dyn std::error::Error + 'a>>> ResultExt<T> for Result<T, E> {
271	#[track_caller]
272	fn unrecoverable_error(self, context: &str) -> Result<T, InternalServerError> {
273		match self {
274			Ok(value) => Ok(value),
275			Err(err) => {
276				let location = Location::caller();
277				let err = err.into();
278				tracing::error!(?err, context, ?location, "Encountered unrecoverable error");
279				Err(InternalServerError)
280			}
281		}
282	}
283}
284
285impl<T> ResultExt<T> for Option<T> {
286	#[track_caller]
287	fn unrecoverable_error(self, context: &str) -> Result<T, InternalServerError> {
288		match self {
289			Some(value) => Ok(value),
290			None => {
291				let location = Location::caller();
292				tracing::error!(context, ?location, "Expected some");
293				Err(InternalServerError)
294			}
295		}
296	}
297}
298
299impl InternalServerError {
300	/// Check that the condition `cond` is true, and if not, return an internal
301	/// server error, and log the context string and line where the issue arose
302	#[track_caller]
303	pub fn assert(cond: bool, context: &str) -> Result<(), InternalServerError> {
304		if cond {
305			Ok(())
306		} else {
307			let location = Location::caller();
308			tracing::error!(context, ?location, "Assertion failed");
309			Err(InternalServerError)
310		}
311	}
312}