1 use serde::{Deserialize, Serialize};
6 use tracing_error::SpanTrace;
7 #[cfg(feature = "full")]
10 pub type LemmyResult<T> = Result<T, LemmyError>;
12 pub struct LemmyError {
13 pub error_type: LemmyErrorType,
14 pub inner: anyhow::Error,
15 pub context: SpanTrace,
18 impl<T> From<T> for LemmyError
20 T: Into<anyhow::Error>,
22 fn from(t: T) -> Self {
25 error_type: LemmyErrorType::Unknown(format!("{}", &cause)),
27 context: SpanTrace::capture(),
32 impl Debug for LemmyError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.debug_struct("LemmyError")
35 .field("message", &self.error_type)
36 .field("inner", &self.inner)
37 .field("context", &self.context)
42 impl Display for LemmyError {
43 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44 write!(f, "{}: ", &self.error_type)?;
45 // print anyhow including trace
46 // https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
47 // this will print the anyhow trace (only if it exists)
48 // and if RUST_BACKTRACE=1, also a full backtrace
49 writeln!(f, "{:?}", self.inner)?;
50 fmt::Display::fmt(&self.context, f)
54 impl actix_web::error::ResponseError for LemmyError {
55 fn status_code(&self) -> http::StatusCode {
56 match self.inner.downcast_ref::<diesel::result::Error>() {
57 Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND,
58 _ => http::StatusCode::BAD_REQUEST,
62 fn error_response(&self) -> actix_web::HttpResponse {
63 actix_web::HttpResponse::build(self.status_code()).json(&self.error_type)
67 #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter)]
68 #[cfg_attr(feature = "full", derive(TS))]
69 #[cfg_attr(feature = "full", ts(export))]
70 #[serde(tag = "error", content = "message", rename_all = "snake_case")]
71 // TODO: order these based on the crate they belong to (utils, federation, db, api)
72 pub enum LemmyErrorType {
84 CouldntUpdatePrivateMessage,
87 SiteMetadataPageIsNotDoctypeHtml,
88 PictrsResponseError(String),
89 PictrsPurgeResponseError(String),
90 ImageUrlMissingPathSegments,
91 ImageUrlMissingLastPathSegment,
92 PictrsApiKeyNotProvided,
103 CouldntFindCommunity,
106 DownvotesAreDisabled,
109 SiteDescriptionLengthOverflow,
111 RegistrationApplicationIsPending,
112 CantEnablePrivateInstanceAndFederationTogether,
114 CouldntCreateComment,
115 MaxCommentDepthReached,
116 NoCommentEditAllowed,
117 OnlyAdminsCanCreateCommunities,
118 CommunityAlreadyExists,
120 OnlyModsCanPostInCommunity,
124 EditPrivateMessageNotAllowed,
126 ApplicationQuestionRequired,
127 InvalidDefaultPostListingType,
129 RegistrationApplicationAnswerRequired,
131 FederationForbiddenByStrictAllowList,
132 PersonIsBannedFromCommunity,
135 CannotCreatePostOrCommentInDeletedOrRemovedCommunity,
137 NewPostCannotBeLocked,
138 OnlyLocalAdminCanRemoveCommunity,
139 OnlyLocalAdminCanRestoreCommunity,
145 PersonIsBannedFromSite(String),
147 PageDoesNotSpecifyCreator,
148 PageDoesNotSpecifyGroup,
149 NoCommunityFoundInCc,
151 EmailSmtpServerNeedsAPort,
162 CouldntParseTotpSecret,
166 CouldntResolveReport,
167 CommunityModeratorAlreadyExists,
168 CommunityUserAlreadyBanned,
169 CommunityBlockAlreadyExists,
170 CommunityFollowerAlreadyExists,
171 CouldntUpdateCommunityHiddenStatus,
172 PersonBlockAlreadyExists,
177 CouldntMarkPostAsRead,
178 CouldntUpdateCommunity,
179 CouldntUpdateReplies,
180 CouldntUpdatePersonMentions,
183 CouldntCreatePrivateMessage,
184 CouldntUpdatePrivate,
186 CouldntSetAllRegistrationsAccepted,
187 CouldntSetAllEmailVerified,
196 RegistrationDenied(String),
198 DomainBlocked(String),
199 DomainNotInAllowList(String),
200 FederationDisabledByStrictAllowList,
202 SiteNameLengthOverflow,
206 PasswordResetLimitReached,
207 CouldntCreateAudioCaptcha,
209 CouldntSendWebmention,
210 ContradictingFilters,
214 impl From<LemmyErrorType> for LemmyError {
215 fn from(error_type: LemmyErrorType) -> Self {
216 let inner = anyhow::anyhow!("{}", error_type);
220 context: SpanTrace::capture(),
225 pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
226 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
229 impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
230 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
231 self.map_err(|error| LemmyError {
234 context: SpanTrace::capture(),
238 pub trait LemmyErrorExt2<T> {
239 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
242 impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
243 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
244 self.map_err(|mut e| {
245 e.error_type = error_type;
253 #![allow(clippy::unwrap_used)]
254 #![allow(clippy::indexing_slicing)]
256 use actix_web::{body::MessageBody, ResponseError};
257 use std::fs::read_to_string;
258 use strum::IntoEnumIterator;
261 fn deserializes_no_message() {
262 let err = LemmyError::from(LemmyErrorType::Banned).error_response();
263 let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
264 assert_eq!(&json, "{\"error\":\"banned\"}")
268 fn deserializes_with_message() {
269 let reg_denied = LemmyErrorType::RegistrationDenied(String::from("reason"));
270 let err = LemmyError::from(reg_denied).error_response();
271 let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
274 "{\"error\":\"registration_denied\",\"message\":\"reason\"}"
278 /// Check if errors match translations. Disabled because many are not translated at all.
281 fn test_translations_match() {
282 #[derive(Deserialize)]
287 let translations = read_to_string("translations/translations/en.json").unwrap();
288 LemmyErrorType::iter().for_each(|e| {
289 let msg = serde_json::to_string(&e).unwrap();
290 let msg: Err = serde_json::from_str(&msg).unwrap();
292 assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");