X-Git-Url: http://these/git/?a=blobdiff_plain;f=crates%2Futils%2Fsrc%2Ferror.rs;h=ffc1723b4f75e79e8c440787cf72b5156a806ed6;hb=92568956353f21649ed9aff68b42699c9d036f30;hp=441c0422e6637ac2b6c4f8c566147aeb27c01fc4;hpb=a2a594b7635db2241602be56250f7d9bf992f7b9;p=lemmy.git diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 441c0422..ffc1723b 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -1,73 +1,29 @@ +use serde::{Deserialize, Serialize}; use std::{ fmt, fmt::{Debug, Display}, }; use tracing_error::SpanTrace; +#[cfg(feature = "full")] +use ts_rs::TS; -#[derive(serde::Serialize)] -struct ApiError { - error: String, -} +pub type LemmyResult = Result; pub struct LemmyError { - pub message: Option, + pub error_type: LemmyErrorType, pub inner: anyhow::Error, pub context: SpanTrace, } -impl LemmyError { - /// Create LemmyError from a message, including stack trace - pub fn from_message(message: &str) -> Self { - let inner = anyhow::anyhow!("{}", message); - LemmyError { - message: Some(message.into()), - inner, - context: SpanTrace::capture(), - } - } - - /// Create a LemmyError from error and message, including stack trace - pub fn from_error_message(error: E, message: &str) -> Self - where - E: Into, - { - LemmyError { - message: Some(message.into()), - inner: error.into(), - context: SpanTrace::capture(), - } - } - - /// Add message to existing LemmyError (or overwrite existing error) - pub fn with_message(self, message: &str) -> Self { - LemmyError { - message: Some(message.into()), - ..self - } - } - - pub fn to_json(&self) -> Result { - let api_error = match &self.message { - Some(error) => ApiError { - error: error.into(), - }, - None => ApiError { - error: "Unknown".into(), - }, - }; - - Ok(serde_json::to_string(&api_error)?) - } -} - impl From for LemmyError where T: Into, { fn from(t: T) -> Self { + let cause = t.into(); LemmyError { - message: None, - inner: t.into(), + error_type: LemmyErrorType::Unknown(format!("{}", &cause)), + inner: cause, context: SpanTrace::capture(), } } @@ -76,19 +32,21 @@ where impl Debug for LemmyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LemmyError") - .field("message", &self.message) + .field("message", &self.error_type) .field("inner", &self.inner) - .field("context", &"SpanTrace") + .field("context", &self.context) .finish() } } impl Display for LemmyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(message) = &self.message { - write!(f, "{}: ", message)?; - } - writeln!(f, "{}", self.inner)?; + write!(f, "{}: ", &self.error_type)?; + // print anyhow including trace + // https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations + // this will print the anyhow trace (only if it exists) + // and if RUST_BACKTRACE=1, also a full backtrace + writeln!(f, "{:?}", self.inner)?; fmt::Display::fmt(&self.context, f) } } @@ -102,14 +60,235 @@ impl actix_web::error::ResponseError for LemmyError { } fn error_response(&self) -> actix_web::HttpResponse { - if let Some(message) = &self.message { - actix_web::HttpResponse::build(self.status_code()).json(ApiError { - error: message.into(), - }) - } else { - actix_web::HttpResponse::build(self.status_code()) - .content_type("text/plain") - .body(self.inner.to_string()) + actix_web::HttpResponse::build(self.status_code()).json(&self.error_type) + } +} + +#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +#[serde(tag = "error", content = "message", rename_all = "snake_case")] +// TODO: order these based on the crate they belong to (utils, federation, db, api) +pub enum LemmyErrorType { + ReportReasonRequired, + ReportTooLong, + NotAModerator, + NotAnAdmin, + CantBlockYourself, + CantBlockAdmin, + CouldntUpdateUser, + PasswordsDoNotMatch, + EmailNotVerified, + EmailRequired, + CouldntUpdateComment, + CouldntUpdatePrivateMessage, + CannotLeaveAdmin, + NoLinesInHtml, + SiteMetadataPageIsNotDoctypeHtml, + PictrsResponseError(String), + PictrsPurgeResponseError(String), + ImageUrlMissingPathSegments, + ImageUrlMissingLastPathSegment, + PictrsApiKeyNotProvided, + NoContentTypeHeader, + NotAnImageType, + NotAModOrAdmin, + NoAdmins, + NotTopAdmin, + NotTopMod, + NotLoggedIn, + SiteBan, + Deleted, + BannedFromCommunity, + CouldntFindCommunity, + CouldntFindPerson, + PersonIsBlocked, + DownvotesAreDisabled, + InstanceIsPrivate, + InvalidPassword, + SiteDescriptionLengthOverflow, + HoneypotFailed, + RegistrationApplicationIsPending, + CantEnablePrivateInstanceAndFederationTogether, + Locked, + CouldntCreateComment, + MaxCommentDepthReached, + NoCommentEditAllowed, + OnlyAdminsCanCreateCommunities, + CommunityAlreadyExists, + LanguageNotAllowed, + OnlyModsCanPostInCommunity, + CouldntUpdatePost, + NoPostEditAllowed, + CouldntFindPost, + EditPrivateMessageNotAllowed, + SiteAlreadyExists, + ApplicationQuestionRequired, + InvalidDefaultPostListingType, + RegistrationClosed, + RegistrationApplicationAnswerRequired, + EmailAlreadyExists, + FederationForbiddenByStrictAllowList, + PersonIsBannedFromCommunity, + ObjectIsNotPublic, + InvalidCommunity, + CannotCreatePostOrCommentInDeletedOrRemovedCommunity, + CannotReceivePage, + NewPostCannotBeLocked, + OnlyLocalAdminCanRemoveCommunity, + OnlyLocalAdminCanRestoreCommunity, + NoIdGiven, + IncorrectLogin, + InvalidQuery, + ObjectNotLocal, + PostIsLocked, + PersonIsBannedFromSite, + InvalidVoteValue, + PageDoesNotSpecifyCreator, + PageDoesNotSpecifyGroup, + NoCommunityFoundInCc, + NoEmailSetup, + EmailSmtpServerNeedsAPort, + MissingAnEmail, + RateLimitError, + InvalidName, + InvalidDisplayName, + InvalidMatrixId, + InvalidPostTitle, + InvalidBodyField, + BioLengthOverflow, + MissingTotpToken, + IncorrectTotpToken, + CouldntParseTotpSecret, + CouldntLikeComment, + CouldntSaveComment, + CouldntCreateReport, + CouldntResolveReport, + CommunityModeratorAlreadyExists, + CommunityUserAlreadyBanned, + CommunityBlockAlreadyExists, + CommunityFollowerAlreadyExists, + CouldntUpdateCommunityHiddenStatus, + PersonBlockAlreadyExists, + UserAlreadyExists, + TokenNotFound, + CouldntLikePost, + CouldntSavePost, + CouldntMarkPostAsRead, + CouldntUpdateCommunity, + CouldntUpdateReplies, + CouldntUpdatePersonMentions, + PostTitleTooLong, + CouldntCreatePost, + CouldntCreatePrivateMessage, + CouldntUpdatePrivate, + SystemErrLogin, + CouldntSetAllRegistrationsAccepted, + CouldntSetAllEmailVerified, + Banned, + CouldntGetComments, + CouldntGetPosts, + InvalidUrl, + EmailSendFailed, + Slurs, + CouldntGenerateTotp, + CouldntFindObject, + RegistrationDenied(String), + FederationDisabled, + DomainBlocked, + DomainNotInAllowList, + FederationDisabledByStrictAllowList, + SiteNameRequired, + SiteNameLengthOverflow, + PermissiveRegex, + InvalidRegex, + CaptchaIncorrect, + PasswordResetLimitReached, + CouldntCreateAudioCaptcha, + InvalidUrlScheme, + CouldntSendWebmention, + Unknown(String), +} + +impl From for LemmyError { + fn from(error_type: LemmyErrorType) -> Self { + let inner = anyhow::anyhow!("{}", error_type); + LemmyError { + error_type, + inner, + context: SpanTrace::capture(), } } } + +pub trait LemmyErrorExt> { + fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result; +} + +impl> LemmyErrorExt for Result { + fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result { + self.map_err(|error| LemmyError { + error_type, + inner: error.into(), + context: SpanTrace::capture(), + }) + } +} +pub trait LemmyErrorExt2 { + fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result; +} + +impl LemmyErrorExt2 for Result { + fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result { + self.map_err(|mut e| { + e.error_type = error_type; + e + }) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + use super::*; + use actix_web::{body::MessageBody, ResponseError}; + use std::fs::read_to_string; + use strum::IntoEnumIterator; + + #[test] + fn deserializes_no_message() { + let err = LemmyError::from(LemmyErrorType::Banned).error_response(); + let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); + assert_eq!(&json, "{\"error\":\"banned\"}") + } + + #[test] + fn deserializes_with_message() { + let reg_denied = LemmyErrorType::RegistrationDenied(String::from("reason")); + let err = LemmyError::from(reg_denied).error_response(); + let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); + assert_eq!( + &json, + "{\"error\":\"registration_denied\",\"message\":\"reason\"}" + ) + } + + /// Check if errors match translations. Disabled because many are not translated at all. + #[test] + #[ignore] + fn test_translations_match() { + #[derive(Deserialize)] + struct Err { + error: String, + } + + let translations = read_to_string("translations/translations/en.json").unwrap(); + LemmyErrorType::iter().for_each(|e| { + let msg = serde_json::to_string(&e).unwrap(); + let msg: Err = serde_json::from_str(&msg).unwrap(); + let msg = msg.error; + assert!(translations.contains(&format!("\"{msg}\"")), "{msg}"); + }); + } +}