{-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} module Gargantext.API.Errors ( module Types , module Class -- * Types , GargErrorScheme(..) , renderGargErrorScheme -- * Conversion functions , backendErrorToFrontendError , frontendErrorToServerError , frontendErrorToGQLServerError -- * Temporary shims , showAsServantJSONErr ) where import Prelude import Control.Exception.Safe import Data.Aeson qualified as JSON import Data.Text qualified as T import Data.Text.Lazy qualified as TL import Data.Text.Lazy.Encoding qualified as TE import Data.Validity ( prettyValidation ) import Gargantext.API.Admin.Auth.Types import Gargantext.API.Errors.Class as Class import Gargantext.API.Errors.TH (deriveHttpStatusCode) import Gargantext.API.Errors.Types as Types import Gargantext.Database.Query.Table.Node.Error hiding (nodeError) import Gargantext.Database.Query.Tree hiding (treeError) import Gargantext.Utils.Jobs.Monad (JobError(..)) import Network.HTTP.Types.Status qualified as HTTP import Servant.Server $(deriveHttpStatusCode ''BackendErrorCode) data GargErrorScheme = -- | The old error scheme. GES_old -- | The new error scheme, that returns a 'FrontendError'. | GES_new -- | Error scheme for GraphQL, has to be slightly different -- {errors: [{message, extensions: { ... }}]} -- https://spec.graphql.org/June2018/#sec-Errors deriving (Show, Eq) renderGargErrorScheme :: GargErrorScheme -> T.Text renderGargErrorScheme = \case GES_old -> "old" GES_new -> "new" -- | Transforms a backend internal error into something that the frontend -- can consume. This is the only representation we offer to the outside world, -- as we later encode this into a 'ServerError' in the main server handler. backendErrorToFrontendError :: BackendInternalError -> FrontendError backendErrorToFrontendError = \case InternalAuthenticationError authError -> authErrorToFrontendError authError InternalNodeError nodeError -> nodeErrorToFrontendError nodeError InternalJobError jobError -> jobErrorToFrontendError jobError InternalServerError internalServerError -> internalServerErrorToFrontendError internalServerError InternalTreeError treeError -> treeErrorToFrontendError treeError -- As this carries a 'SomeException' which might exposes sensible -- information, we do not send to the frontend its content. InternalUnexpectedError _ -> let msg = T.pack $ "An unexpected error occurred. Please check your server logs." in mkFrontendErr' msg $ FE_internal_server_error msg InternalValidationError validationError -> mkFrontendErr' "A validation error occurred" $ FE_validation_error $ case prettyValidation validationError of Nothing -> "unknown_validation_error" Just v -> T.pack v frontendErrorToGQLServerError :: FrontendError -> ServerError frontendErrorToGQLServerError fe@(FrontendError diag ty _) = ServerError { errHTTPCode = HTTP.statusCode $ backendErrorTypeToErrStatus ty , errReasonPhrase = T.unpack diag , errBody = JSON.encode (GraphQLError fe) , errHeaders = [("Content-Type", "application/json")] } authErrorToFrontendError :: AuthenticationError -> FrontendError authErrorToFrontendError = \case -- For now, we ignore the Jose error, as they are too specific -- (i.e. they should be logged internally to Sentry rather than shared -- externally). LoginFailed nid uid _ -> mkFrontendErr' "Invalid username/password, or invalid session token." $ FE_login_failed_error nid uid InvalidUsernameOrPassword -> mkFrontendErr' "Invalid username or password." $ FE_login_failed_invalid_username_or_password UserNotAuthorized uId msg -> mkFrontendErr' "User not authorized. " $ FE_user_not_authorized uId msg -- | Converts a 'FrontendError' into a 'ServerError' that the servant app can -- return to the frontend. frontendErrorToServerError :: FrontendError -> ServerError frontendErrorToServerError fe@(FrontendError diag ty _) = ServerError { errHTTPCode = HTTP.statusCode $ backendErrorTypeToErrStatus ty , errReasonPhrase = T.unpack diag , errBody = JSON.encode fe , errHeaders = mempty } internalServerErrorToFrontendError :: ServerError -> FrontendError internalServerErrorToFrontendError ServerError{..} | errHTTPCode == 405 = mkFrontendErr' (T.pack errReasonPhrase) $ FE_not_allowed (TL.toStrict $ TE.decodeUtf8 $ errBody) | otherwise = mkFrontendErr' (T.pack errReasonPhrase) $ FE_internal_server_error (TL.toStrict $ TE.decodeUtf8 $ errBody) jobErrorToFrontendError :: JobError -> FrontendError jobErrorToFrontendError = \case InvalidIDType idTy -> mkFrontendErrNoDiagnostic $ FE_job_invalid_id_type idTy IDExpired jobId -> mkFrontendErrNoDiagnostic $ FE_job_expired jobId InvalidMacID macId -> mkFrontendErrNoDiagnostic $ FE_job_invalid_mac macId UnknownJob jobId -> mkFrontendErrNoDiagnostic $ FE_job_unknown_job jobId JobException err -> mkFrontendErrNoDiagnostic $ FE_job_generic_exception (T.pack $ displayException err) nodeErrorToFrontendError :: NodeError -> FrontendError nodeErrorToFrontendError ne = case ne of NoListFound lid -> mkFrontendErrShow $ FE_node_list_not_found lid NoRootFound -> mkFrontendErrShow FE_node_root_not_found NoCorpusFound -> mkFrontendErrShow FE_node_corpus_not_found NodeCreationFailed reason -> case reason of UserParentAlreadyExists pId uId -> mkFrontendErrShow $ FE_node_creation_failed_parent_exists uId pId UserParentDoesNotExist uId -> mkFrontendErrShow $ FE_node_creation_failed_no_parent uId InsertNodeFailed uId pId -> mkFrontendErrShow $ FE_node_creation_failed_insert_node uId pId UserHasNegativeId uid -> mkFrontendErrShow $ FE_node_creation_failed_user_negative_id uid NodeLookupFailed reason -> case reason of NodeDoesNotExist nid -> mkFrontendErrShow $ FE_node_lookup_failed_not_found nid NodeParentDoesNotExist nid -> mkFrontendErrShow $ FE_node_lookup_failed_parent_not_found nid UserDoesNotExist uid -> mkFrontendErrShow $ FE_node_lookup_failed_user_not_found uid UserNameDoesNotExist uname -> mkFrontendErrShow $ FE_node_lookup_failed_username_not_found uname UserHasTooManyRoots uid roots -> mkFrontendErrShow $ FE_node_lookup_failed_user_too_many_roots uid roots NotImplYet -> mkFrontendErrShow FE_node_not_implemented_yet NoContextFound contextId -> mkFrontendErrShow $ FE_node_context_not_found contextId NeedsConfiguration -> mkFrontendErrShow $ FE_node_needs_configuration NodeError err -> mkFrontendErrShow $ FE_node_generic_exception (T.pack $ displayException err) -- backward-compatibility shims, to remove eventually. DoesNotExist nid -> mkFrontendErrShow $ FE_node_lookup_failed_not_found nid treeErrorToFrontendError :: TreeError -> FrontendError treeErrorToFrontendError te = case te of NoRoot -> mkFrontendErrShow FE_tree_root_not_found EmptyRoot -> mkFrontendErrShow FE_tree_empty_root TooManyRoots roots -> mkFrontendErrShow $ FE_tree_too_many_roots roots showAsServantJSONErr :: BackendInternalError -> ServerError showAsServantJSONErr (InternalNodeError err@(NoListFound {})) = err404 { errBody = JSON.encode err } showAsServantJSONErr (InternalNodeError err@NoRootFound{}) = err404 { errBody = JSON.encode err } showAsServantJSONErr (InternalNodeError err@NoCorpusFound) = err404 { errBody = JSON.encode err } showAsServantJSONErr (InternalNodeError err@(DoesNotExist {})) = err404 { errBody = JSON.encode err } showAsServantJSONErr (InternalServerError err) = err showAsServantJSONErr a = err500 { errBody = JSON.encode a }