packages feed

hackage-server-0.4: Distribution/Server/Framework/Error.hs

-- | Error handling style for the hackage server.
--
-- The point is to be able to abort and return appropriate HTTP errors, plus
-- human readable messages.
--
-- We use a standard error monad / exception style so that we can hide some of
-- the error checking plumbing.
--
-- We use a custom error type that enables us to render the error in an
-- appropriate way, ie themed html or plain text, depending on the context.
--
module Distribution.Server.Framework.Error (

    -- * Server error monad
    ServerPartE,

    -- * Generating errors
    MessageSpan(..),
    errBadRequest,
    errForbidden,
    errNotFound,
    errBadMediaType,
    errInternalError,
    throwError,

    -- * Handling errors
    ErrorResponse(..),
    runServerPartE,
    handleErrorResponse,
    messageToText,

    -- * Handy error message operator
    (?!)
  ) where

import Happstack.Server
import Control.Monad.Error

import qualified Data.Text.Lazy          as Text
import qualified Data.Text.Lazy.Encoding as Text

-- | The \"oh noes?!\" operator
--
(?!) :: Maybe a -> e -> Either e a
ma ?! e = maybe (Left e) Right ma


-- | A derivative of the 'ServerPartT' monad with an extra error monad layer.
--
-- So we can use the standard 'MonadError' methods like 'throwError'.
--
type ServerPartE a = ServerPartT (ErrorT ErrorResponse IO) a

-- | A type for generic error reporting that should be sufficient for
-- most purposes.
--
data ErrorResponse = ErrorResponse {
    errorCode   :: Int,
    errorHeaders:: [(String, String)],
    errorTitle  :: String,
    errorDetail :: [MessageSpan]
} deriving (Eq, Show)

instance ToMessage ErrorResponse where
  toResponse (ErrorResponse code hdrs title detail) =
    let rspbody = title ++ ": " ++ messageToText detail ++ "\n"
     in Response {
          rsCode      = code,
          rsHeaders   = mkHeaders (("Content-Type",  "text/plain") : reverse hdrs),
          rsFlags     = nullRsFlags { rsfLength = ContentLength },
          rsBody      = Text.encodeUtf8 (Text.pack rspbody),
          rsValidator = Nothing
        }

-- | A message possibly including hypertext links.
--
-- The point is to be able to render error messages either as text or as html.
--
data MessageSpan = MLink String String | MText String
  deriving (Eq, Show)

-- | Format a message as simple text.
--
-- For html or other formats you'll have to write your own function!
--
messageToText :: [MessageSpan] -> String
messageToText []             = ""
messageToText (MLink x _:xs) = x ++ messageToText xs
messageToText (MText x  :xs) = x ++ messageToText xs

-- We don't want to use these methods directly anyway.
instance Error ErrorResponse where
    noMsg      = ErrorResponse 500 [] "Internal server error" []
    strMsg str = ErrorResponse 500 [] "Internal server error" [MText str]


errBadRequest    :: String -> [MessageSpan] -> ServerPartE a
errBadRequest    title message = throwError (ErrorResponse 400 [] title message)

-- note: errUnauthorized is deliberately not provided because exceptions thrown
-- in this way bypass the FilterMonad stuff and so setHeaderM etc are ignored
-- but setHeaderM are usually needed for responding to auth errors.

errForbidden     :: String -> [MessageSpan] -> ServerPartE a
errForbidden     title message = throwError (ErrorResponse 403 [] title message)

errNotFound      :: String -> [MessageSpan] -> ServerPartE a
errNotFound      title message = throwError (ErrorResponse 404 [] title message)

errBadMediaType  :: String -> [MessageSpan] -> ServerPartE a
errBadMediaType  title message = throwError (ErrorResponse 415 [] title message)

errInternalError :: [MessageSpan] -> ServerPartE a
errInternalError       message = throwError (ErrorResponse 500 [] title message)
  where
    title = "Internal server error"

-- | Run a 'ServerPartE', including a top-level fallback error handler.
--
-- Any 'ErrorResponse' exceptions are turned into a simple error response with
-- a \"text/plain\" formated body.
--
-- To use a nicer custom formatted error response, use 'handleErrorResponse'.
--
runServerPartE :: ServerPartE a -> ServerPart a
runServerPartE = mapServerPartT' (spUnwrapErrorT fallbackHandler)
  where
    fallbackHandler :: ErrorResponse -> ServerPart a
    fallbackHandler err = finishWith (toResponse err)

handleErrorResponse :: (ErrorResponse -> ServerPartE Response)
                    -> ServerPartE a -> ServerPartE a
handleErrorResponse handler action =
    catchError action (\errResp -> handler errResp >>= finishWith)