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