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,
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),
199 DomainNotInAllowList,
200 FederationDisabledByStrictAllowList,
202 SiteNameLengthOverflow,
206 PasswordResetLimitReached,
207 CouldntCreateAudioCaptcha,
209 CouldntSendWebmention,
213 impl From<LemmyErrorType> for LemmyError {
214 fn from(error_type: LemmyErrorType) -> Self {
215 let inner = anyhow::anyhow!("{}", error_type);
219 context: SpanTrace::capture(),
224 pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
225 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
228 impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
229 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
230 self.map_err(|error| LemmyError {
233 context: SpanTrace::capture(),
237 pub trait LemmyErrorExt2<T> {
238 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
241 impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
242 fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
243 self.map_err(|mut e| {
244 e.error_type = error_type;
253 use actix_web::{body::MessageBody, ResponseError};
254 use std::fs::read_to_string;
255 use strum::IntoEnumIterator;
258 fn deserializes_no_message() {
259 let err = LemmyError::from(LemmyErrorType::Banned).error_response();
260 let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
261 assert_eq!(&json, "{\"error\":\"banned\"}")
265 fn deserializes_with_message() {
266 let reg_denied = LemmyErrorType::RegistrationDenied(String::from("reason"));
267 let err = LemmyError::from(reg_denied).error_response();
268 let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
271 "{\"error\":\"registration_denied\",\"message\":\"reason\"}"
275 /// Check if errors match translations. Disabled because many are not translated at all.
278 fn test_translations_match() {
279 #[derive(Deserialize)]
284 let translations = read_to_string("translations/translations/en.json").unwrap();
285 LemmyErrorType::iter().for_each(|e| {
286 let msg = serde_json::to_string(&e).unwrap();
287 let msg: Err = serde_json::from_str(&msg).unwrap();
289 assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");