/ src / Gargantext / API / Errors.hs
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 }