Errors.hs
1 {-# LANGUAGE LambdaCase #-} 2 {-# LANGUAGE TemplateHaskell #-} 3 {-# LANGUAGE TypeApplications #-} 4 5 module Gargantext.API.Errors ( 6 module Types 7 , module Class 8 9 -- * Types 10 , GargErrorScheme(..) 11 12 -- * Conversion functions 13 , backendErrorToFrontendError 14 , frontendErrorToServerError 15 , frontendErrorToGQLServerError 16 17 -- * Temporary shims 18 , showAsServantJSONErr 19 ) where 20 21 import Prelude 22 23 import Control.Exception 24 import Data.Aeson qualified as JSON 25 import Data.Text qualified as T 26 import Data.Text.Lazy qualified as TL 27 import Data.Text.Lazy.Encoding qualified as TE 28 import Data.Validity ( prettyValidation ) 29 import Gargantext.API.Admin.Auth.Types 30 import Gargantext.API.Errors.Class as Class 31 import Gargantext.API.Errors.TH (deriveHttpStatusCode) 32 import Gargantext.API.Errors.Types as Types 33 import Gargantext.Database.Query.Table.Node.Error hiding (nodeError) 34 import Gargantext.Database.Query.Tree hiding (treeError) 35 import Gargantext.Utils.Jobs.Monad (JobError(..)) 36 import Network.HTTP.Types.Status qualified as HTTP 37 import Servant.Server 38 39 $(deriveHttpStatusCode ''BackendErrorCode) 40 41 data GargErrorScheme 42 = -- | The old error scheme. 43 GES_old 44 -- | The new error scheme, that returns a 'FrontendError'. 45 | GES_new 46 -- | Error scheme for GraphQL, has to be slightly different 47 -- {errors: [{message, extensions: { ... }}]} 48 -- https://spec.graphql.org/June2018/#sec-Errors 49 deriving (Show, Eq) 50 51 -- | Transforms a backend internal error into something that the frontend 52 -- can consume. This is the only representation we offer to the outside world, 53 -- as we later encode this into a 'ServerError' in the main server handler. 54 backendErrorToFrontendError :: BackendInternalError -> FrontendError 55 backendErrorToFrontendError = \case 56 InternalAuthenticationError authError 57 -> authErrorToFrontendError authError 58 InternalNodeError nodeError 59 -> nodeErrorToFrontendError nodeError 60 InternalJobError jobError 61 -> jobErrorToFrontendError jobError 62 InternalServerError internalServerError 63 -> internalServerErrorToFrontendError internalServerError 64 InternalTreeError treeError 65 -> treeErrorToFrontendError treeError 66 -- As this carries a 'SomeException' which might exposes sensible 67 -- information, we do not send to the frontend its content. 68 InternalUnexpectedError _ 69 -> let msg = T.pack $ "An unexpected error occurred. Please check your server logs." 70 in mkFrontendErr' msg $ FE_internal_server_error msg 71 InternalValidationError validationError 72 -> mkFrontendErr' "A validation error occurred" 73 $ FE_validation_error $ case prettyValidation validationError of 74 Nothing -> "unknown_validation_error" 75 Just v -> T.pack v 76 77 frontendErrorToGQLServerError :: FrontendError -> ServerError 78 frontendErrorToGQLServerError fe@(FrontendError diag ty _) = 79 ServerError { errHTTPCode = HTTP.statusCode $ backendErrorTypeToErrStatus ty 80 , errReasonPhrase = T.unpack diag 81 , errBody = JSON.encode (GraphQLError fe) 82 , errHeaders = [("Content-Type", "application/json")] 83 } 84 85 authErrorToFrontendError :: AuthenticationError -> FrontendError 86 authErrorToFrontendError = \case 87 -- For now, we ignore the Jose error, as they are too specific 88 -- (i.e. they should be logged internally to Sentry rather than shared 89 -- externally). 90 LoginFailed nid uid _ 91 -> mkFrontendErr' "Invalid username/password, or invalid session token." $ FE_login_failed_error nid uid 92 InvalidUsernameOrPassword 93 -> mkFrontendErr' "Invalid username or password." $ FE_login_failed_invalid_username_or_password 94 UserNotAuthorized uId msg 95 -> mkFrontendErr' "User not authorized. " $ FE_user_not_authorized uId msg 96 97 -- | Converts a 'FrontendError' into a 'ServerError' that the servant app can 98 -- return to the frontend. 99 frontendErrorToServerError :: FrontendError -> ServerError 100 frontendErrorToServerError fe@(FrontendError diag ty _) = 101 ServerError { errHTTPCode = HTTP.statusCode $ backendErrorTypeToErrStatus ty 102 , errReasonPhrase = T.unpack diag 103 , errBody = JSON.encode fe 104 , errHeaders = mempty 105 } 106 107 internalServerErrorToFrontendError :: ServerError -> FrontendError 108 internalServerErrorToFrontendError = \case 109 ServerError{..} 110 | errHTTPCode == 405 111 -> mkFrontendErr' (T.pack errReasonPhrase) $ FE_not_allowed (TL.toStrict $ TE.decodeUtf8 $ errBody) 112 | otherwise 113 -> mkFrontendErr' (T.pack errReasonPhrase) $ FE_internal_server_error (TL.toStrict $ TE.decodeUtf8 $ errBody) 114 115 jobErrorToFrontendError :: JobError -> FrontendError 116 jobErrorToFrontendError = \case 117 InvalidIDType idTy -> mkFrontendErrNoDiagnostic $ FE_job_invalid_id_type idTy 118 IDExpired jobId -> mkFrontendErrNoDiagnostic $ FE_job_expired jobId 119 InvalidMacID macId -> mkFrontendErrNoDiagnostic $ FE_job_invalid_mac macId 120 UnknownJob jobId -> mkFrontendErrNoDiagnostic $ FE_job_unknown_job jobId 121 JobException err -> mkFrontendErrNoDiagnostic $ FE_job_generic_exception (T.pack $ displayException err) 122 123 nodeErrorToFrontendError :: NodeError -> FrontendError 124 nodeErrorToFrontendError ne = case ne of 125 NoListFound lid 126 -> mkFrontendErrShow $ FE_node_list_not_found lid 127 NoRootFound 128 -> mkFrontendErrShow FE_node_root_not_found 129 NoCorpusFound 130 -> mkFrontendErrShow FE_node_corpus_not_found 131 NoUserFound _ur 132 -> undefined 133 NodeCreationFailed reason 134 -> case reason of 135 UserParentAlreadyExists pId uId 136 -> mkFrontendErrShow $ FE_node_creation_failed_parent_exists uId pId 137 UserParentDoesNotExist uId 138 -> mkFrontendErrShow $ FE_node_creation_failed_no_parent uId 139 InsertNodeFailed uId pId 140 -> mkFrontendErrShow $ FE_node_creation_failed_insert_node uId pId 141 UserHasNegativeId uid 142 -> mkFrontendErrShow $ FE_node_creation_failed_user_negative_id uid 143 NodeLookupFailed reason 144 -> case reason of 145 NodeDoesNotExist nid 146 -> mkFrontendErrShow $ FE_node_lookup_failed_not_found nid 147 NodeParentDoesNotExist nid 148 -> mkFrontendErrShow $ FE_node_lookup_failed_parent_not_found nid 149 UserDoesNotExist uid 150 -> mkFrontendErrShow $ FE_node_lookup_failed_user_not_found uid 151 UserNameDoesNotExist uname 152 -> mkFrontendErrShow $ FE_node_lookup_failed_username_not_found uname 153 UserHasTooManyRoots uid roots 154 -> mkFrontendErrShow $ FE_node_lookup_failed_user_too_many_roots uid roots 155 NotImplYet 156 -> mkFrontendErrShow FE_node_not_implemented_yet 157 NoContextFound contextId 158 -> mkFrontendErrShow $ FE_node_context_not_found contextId 159 NeedsConfiguration 160 -> mkFrontendErrShow $ FE_node_needs_configuration 161 NodeError err 162 -> mkFrontendErrShow $ FE_node_generic_exception (T.pack $ displayException err) 163 164 -- backward-compatibility shims, to remove eventually. 165 DoesNotExist nid 166 -> mkFrontendErrShow $ FE_node_lookup_failed_not_found nid 167 168 treeErrorToFrontendError :: TreeError -> FrontendError 169 treeErrorToFrontendError te = case te of 170 NoRoot -> mkFrontendErrShow FE_tree_root_not_found 171 EmptyRoot -> mkFrontendErrShow FE_tree_empty_root 172 TooManyRoots roots -> mkFrontendErrShow $ FE_tree_too_many_roots roots 173 174 showAsServantJSONErr :: BackendInternalError -> ServerError 175 showAsServantJSONErr (InternalNodeError err@(NoListFound {})) = err404 { errBody = JSON.encode err } 176 showAsServantJSONErr (InternalNodeError err@NoRootFound{}) = err404 { errBody = JSON.encode err } 177 showAsServantJSONErr (InternalNodeError err@NoCorpusFound) = err404 { errBody = JSON.encode err } 178 showAsServantJSONErr (InternalNodeError err@NoUserFound{}) = err404 { errBody = JSON.encode err } 179 showAsServantJSONErr (InternalNodeError err@(DoesNotExist {})) = err404 { errBody = JSON.encode err } 180 showAsServantJSONErr (InternalServerError err) = err 181 showAsServantJSONErr a = err500 { errBody = JSON.encode a }