-
Notifications
You must be signed in to change notification settings - Fork 26
/
Error.hs
224 lines (186 loc) · 8.03 KB
/
Error.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
{-# LANGUAGE DeriveAnyClass #-}
module Lib.App.Error
( AppError (..)
, AppErrorType
, AppException (..)
, WithError
, throwError
, toHttpError
-- * Error checks
, isServerError
, isNotAllowed
, isInvalid
-- * Internal error helpers
, notFound
, serverError
, notAllowed
, invalid
, missingHeader
, headerDecodeError
, dbError
, dbNamedError
, limitError
-- * Error throwing helpers
, throwOnNothing
, throwOnNothingM
, notFoundOnNothing
, notFoundOnNothingM
) where
import PgNamed (PgNamedError)
import Control.Monad.Except (MonadError)
import Data.CaseInsensitive (foldedCase)
import GHC.Stack (SrcLoc (SrcLoc, srcLocModule, srcLocStartLine))
import Network.HTTP.Types.Header (HeaderName)
import Servant.Server (err401, err404, err413, err417, err500, errBody)
import qualified Control.Monad.Except as E (throwError)
import qualified Servant.Server as Servant (ServerError)
-- | Type alias for errors.
type WithError m = (MonadError AppError m, HasCallStack)
-- | Specialized version of 'E.throwError'
throwError :: WithError m => AppErrorType -> m a
throwError = E.throwError . AppError (toSourcePosition callStack)
{-# INLINE throwError #-}
newtype SourcePosition = SourcePosition Text
deriving newtype (Show, Eq)
-- | Display 'CallStack' as 'SourcePosition' in a format: @Module.function#line_number@.
toSourcePosition :: CallStack -> SourcePosition
toSourcePosition cs = SourcePosition showCallStack
where
showCallStack :: Text
showCallStack = case getCallStack cs of
[] -> "<unknown loc>"
[(name, loc)] -> showLoc name loc
(_, loc) : (callerName, _) : _ -> showLoc callerName loc
showLoc :: String -> SrcLoc -> Text
showLoc name SrcLoc{..} =
toText srcLocModule <> "." <> toText name <> "#" <> show srcLocStartLine
{- | Exception wrapper around 'AppError'. Useful when you need to throw/catch
'AppError' as 'Exception'.
-}
newtype AppException = AppException
{ unAppException :: AppError
} deriving (Show)
deriving anyclass (Exception)
-- | 'AppErrorType' with the corresponding 'CallStack'.
data AppError = AppError
{ appErrorCallStack :: !SourcePosition
, appErrorType :: !AppErrorType
} deriving (Show, Eq)
-- | App errors type.
newtype AppErrorType = InternalError IError
deriving (Show, Eq)
{- | The internal errors that can be thrown. These errors are meant to be
handled within the application and cover exceptional circumstances/coding errors.
-}
data IError
{- | General not found. -}
= NotFound
{- | Some exceptional circumstance has happened stop execution and return.
Optional text to provide some context in server logs.
-}
| ServerError Text
{- | A required permission level was not met. Optional text to provide some context. -}
| NotAllowed Text
{- | Given inputs do not conform to the expected format or shape. Optional
text to provide some context in server logs.
-}
| Invalid Text
{- | Some header expected, but not present in header list.
-}
| MissingHeader HeaderName
{- | An authentication header that was required was provided but not in a
format that the server can understand
-}
| HeaderDecodeError Text
-- | Data base specific errors.
| DbError Text
-- | Data base named parameters errors.
| DbNamedError PgNamedError
-- | Limits on the multi-request are overflowed.
| LimitError
deriving (Show, Eq)
-- | Map 'AppError' into a HTTP error code.
toHttpError :: AppError -> Servant.ServerError
toHttpError (AppError _callStack errorType) = case errorType of
InternalError err -> case err of
NotFound -> err404
ServerError msg -> err500 { errBody = encodeUtf8 msg }
NotAllowed msg -> err401 { errBody = encodeUtf8 msg }
Invalid msg -> err417 { errBody = encodeUtf8 msg }
MissingHeader name -> err401 { errBody = toLazy $ "Header not found: " <> foldedCase name }
HeaderDecodeError name -> err401 { errBody = encodeUtf8 $ "Unable to decode header: " <> name }
DbError e -> err500 { errBody = encodeUtf8 e }
DbNamedError e -> err500 { errBody = show e }
LimitError -> err413 { errBody = "Request is over the limits"}
-- MobileAppError err -> let errMsg = Proto.ErrorResponse err mempty in
-- err400 { errBody = fromStrict $ encodeMessage errMsg }
-- ExternalError err -> case err of
-- ClientError e -> clientErrortoServantErr e
-- -- _ -> err400 { errBody = "External error" }
-- clientErrortoServantErr :: ServantError -> Servant.ServerError
-- clientErrortoServantErr = \case
-- -- The server returned an error response
-- FailureResponse response ->
-- err500 { errBody = show response }
-- -- The body could not be decoded at the expected type
-- DecodeFailure txt response ->
-- err500 { errBody = encodeUtf8 txt <> show response }
-- -- The content-type of the response is not supported
-- UnsupportedContentType mediaType response ->
-- err415 { errBody = show mediaType <> show response }
-- -- The content-type header is invalid
-- InvalidContentTypeHeader response ->
-- err401 { errBody = show response }
-- -- There was a connection error, and no response was received
-- ConnectionError txt ->
-- err503 { errBody = encodeUtf8 txt }
----------------------------------------------------------------------------
-- Error checks
----------------------------------------------------------------------------
isServerError :: AppErrorType -> Bool
isServerError (InternalError (ServerError _)) = True
isServerError _ = False
isNotAllowed :: AppErrorType -> Bool
isNotAllowed (InternalError (NotAllowed _)) = True
isNotAllowed _ = False
isInvalid :: AppErrorType -> Bool
isInvalid (InternalError (Invalid _)) = True
isInvalid _ = False
----------------------------------------------------------------------------
-- Internal Error helpers
----------------------------------------------------------------------------
notFound :: AppErrorType
notFound = InternalError NotFound
serverError :: Text -> AppErrorType
serverError = InternalError . ServerError
notAllowed :: Text -> AppErrorType
notAllowed = InternalError . NotAllowed
invalid :: Text -> AppErrorType
invalid = InternalError . Invalid
missingHeader :: HeaderName -> AppErrorType
missingHeader = InternalError . MissingHeader
headerDecodeError :: Text -> AppErrorType
headerDecodeError = InternalError . HeaderDecodeError
dbError :: Text -> AppErrorType
dbError = InternalError . DbError
dbNamedError :: PgNamedError -> AppErrorType
dbNamedError = InternalError . DbNamedError
limitError :: AppErrorType
limitError = InternalError LimitError
----------------------------------------------------------------------------
-- Helpers
----------------------------------------------------------------------------
-- | Extract the value from a maybe, throwing the given 'AppError' if
-- the value does not exist
throwOnNothing :: WithError m => AppErrorType -> Maybe a -> m a
throwOnNothing err = withFrozenCallStack . maybe (throwError err) pure
-- | Extract the value from a 'Maybe' in @m@, throwing the given 'AppError' if
-- the value does not exist
throwOnNothingM :: WithError m => AppErrorType -> m (Maybe a) -> m a
throwOnNothingM err action = withFrozenCallStack $ action >>= throwOnNothing err
-- | Similar to 'throwOnNothing' but throws a 'NotFound' if the value does not exist
notFoundOnNothing :: WithError m => Maybe a -> m a
notFoundOnNothing = withFrozenCallStack . throwOnNothing notFound
-- | Similar to 'throwOnNothingM' but throws a 'NotFound' if the value does not exist
notFoundOnNothingM :: WithError m => m (Maybe a) -> m a
notFoundOnNothingM = withFrozenCallStack . throwOnNothingM notFound