{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module ServerErrors where import Control.Exception (Exception) import Control.Monad.Logger (MonadLogger, logError, logWarn) import Data.ByteString.Lazy qualified as Bytes.Lazy import Data.Error.Tree import Network.HTTP.Types qualified as Http import PossehlAnalyticsPrelude data ServerError = ServerError { status :: Http.Status, errBody :: Bytes.Lazy.ByteString } deriving stock (Show) deriving anyclass (Exception) emptyServerError :: Http.Status -> ServerError emptyServerError status = ServerError {status, errBody = ""} -- | Throw a user error. -- -- “User” here is a client using our API, not a human user. -- So we throw a `HTTP 400` error, which means the API was used incorrectly. -- -- We also log the error as a warning, because it probably signifies a programming bug in our client. -- -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). throwUserError :: (MonadLogger m, MonadThrow m) => -- | The error to log & throw to the user Error -> m b throwUserError err = do -- TODO: should we make this into a macro to keep the line numbers? $logWarn (err & errorContext "There was a “user holding it wrong” error, check the client code" & prettyError) throwM ServerError { status = Http.badRequest400, errBody = err & prettyError & textToBytesUtf8 & toLazyBytes } -- | Throw a user error. -- -- “User” here is a client using our API, not a human user. -- So we throw a `HTTP 400` error, which means the API was used incorrectly. -- -- We also log the error as a warning, because it probably signifies a programming bug in our client. -- -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). throwUserErrorTree :: (MonadLogger m, MonadThrow m) => -- | The error to log & throw to the user ErrorTree -> m b throwUserErrorTree err = do -- TODO: should we make this into a macro to keep the line numbers? $logWarn (err & nestedError "There was a “user holding it wrong” error, check the client code" & prettyErrorTree) throwM ServerError { status = Http.badRequest400, errBody = err & prettyErrorTree & textToBytesUtf8 & toLazyBytes } -- | Unwrap the `Either` and if `Left` throw a user error. -- -- Intended to use in a pipeline, e.g.: -- -- @@ -- doSomething -- >>= orUserError "Oh no something did not work" -- >>= doSomethingElse -- @@ -- -- “User” here is a client using our API, not a human user. -- So we throw a `HTTP 400` error, which means the API was used incorrectly. -- -- We also log the error as a warning, because it probably signifies a programming bug in our client. -- -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). orUserError :: (MonadThrow m, MonadLogger m) => -- | The message to add as a context to the error being thrown Text -> -- | Result to unwrap and potentially throw Either Error a -> m a orUserError outerMsg eErrA = orUserErrorTree outerMsg (first singleError eErrA) -- | Unwrap the `Either` and if `Left` throw a user error. Will pretty-print the 'ErrorTree' -- -- Intended to use in a pipeline, e.g.: -- -- @@ -- doSomething -- >>= orUserErrorTree "Oh no something did not work" -- >>= doSomethingElse -- @@ -- -- “User” here is a client using our API, not a human user. -- So we throw a `HTTP 400` error, which means the API was used incorrectly. -- -- We also log the error as a warning, because it probably signifies a programming bug in our client. -- -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). orUserErrorTree :: (MonadThrow m, MonadLogger m) => -- | The message to add as a context to the 'ErrorTree' being thrown Text -> -- | Result to unwrap and potentially throw Either ErrorTree a -> m a orUserErrorTree outerMsg = \case Right a -> pure a Left err -> do -- TODO: this outer message should probably be added as a separate root instead of adding to the root error? let tree = errorTreeContext outerMsg err -- TODO: should we make this into a macro to keep the line numbers? $logWarn (errorTreeContext "There was a “user holding it wrong” error, check the client code" tree & prettyErrorTree) throwM ServerError { status = Http.badRequest400, errBody = tree & prettyErrorTree & textToBytesUtf8 & toLazyBytes } -- | Throw an internal error. -- -- “Internal” here means some assertion that we depend on failed, -- e.g. some database request returned a wrong result/number of results -- or some invariant that we expect to hold failed. -- -- This prints the full error to the log, -- and returns a “HTTP 500” error without the message. -- -- If you want to signify a mishandling of the API (e.g. a wrong request), throw a `userError`. -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). throwInternalError :: (MonadLogger m, MonadThrow m) => -- | The error to log internally Error -> m b throwInternalError err = do -- TODO: should we make this into a macro to keep the line numbers? $logError (err & prettyError) throwM $ emptyServerError Http.internalServerError500 -- | Throw an internal error. -- -- “Internal” here means some assertion that we depend on failed, -- e.g. some database request returned a wrong result/number of results -- or some invariant that we expect to hold failed. -- -- This prints the full error to the log, -- and returns a “HTTP 500” error without the message. -- -- If you want to signify a mishandling of the API (e.g. a wrong request), throw a `userError`. -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). throwInternalErrorTree :: (MonadLogger m, MonadThrow m) => -- | The error to log internally ErrorTree -> m b throwInternalErrorTree err = do -- TODO: should we make this into a macro to keep the line numbers? $logError (err & prettyErrorTree) throwM $ emptyServerError Http.internalServerError500 -- | Unwrap the `Either` and if `Left` throw an internal error. -- -- Intended to use in a pipeline, e.g.: -- -- @@ -- doSomething -- >>= orInternalError "Oh no something did not work" -- >>= doSomethingElse -- @@ -- -- “Internal” here means some assertion that we depend on failed, -- e.g. some database request returned a wrong result/number of results -- or some invariant that we expect to hold failed. -- -- This prints the full error to the log, -- and returns a “HTTP 500” error without the message. -- -- If you want to signify a mishandling of the API (e.g. a wrong request), throw a `userError`. -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). orInternalError :: (MonadThrow m, MonadLogger m) => -- | The message to add as a context to the error being thrown Text -> -- | Result to unwrap and potentially throw Either Error a -> m a orInternalError outerMsg eErrA = orInternalErrorTree outerMsg (first singleError eErrA) -- | Unwrap the `Either` and if `Left` throw an internal error. Will pretty-print the 'ErrorTree'. -- -- Intended to use in a pipeline, e.g.: -- -- @@ -- doSomething -- >>= orInternalErrorTree "Oh no something did not work" -- >>= doSomethingElse -- @@ -- -- “Internal” here means some assertion that we depend on failed, -- e.g. some database request returned a wrong result/number of results -- or some invariant that we expect to hold failed. -- -- This prints the full error to the log, -- and returns a “HTTP 500” error without the message. -- -- If you want to signify a mishandling of the API (e.g. a wrong request), throw a `userError`. -- If you need to display a message to a human user, return a `FrontendResponse` -- or a structured type with translation keys (so we can localize the errors). orInternalErrorTree :: (MonadThrow m, MonadLogger m) => -- | The message to add as a context to the 'ErrorTree' being thrown Text -> -- | Result to unwrap and potentially throw Either ErrorTree a -> m a orInternalErrorTree outerMsg = \case Right a -> pure a Left err -> do -- TODO: this outer message should probably be added as a separate root instead of adding to the root error? let tree = errorTreeContext outerMsg err -- TODO: should we make this into a macro to keep the line numbers? $logError (tree & prettyErrorTree) throwM $ emptyServerError Http.internalServerError500