about summary refs log tree commit diff
path: root/users/Profpatsch/htmx-experiment/src/ServerErrors.hs
{-# 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