@@ -0,0 +1,189 @@
1+# Customizing errors from Servant
2+
3+Servant handles a lot of parsing and validation of the input request. When it can't parse something: query
4+parameters, URL parts or request body, it will return appropriate HTTP codes like 400 Bad Request.
5+
6+These responses will contain the error message in their body without any formatting. However, it is often
7+desirable to be able to provide custom formatting for these error messages, for example, to wrap them in JSON.
8+
9+Recently Servant got a way to add such formatting. This Cookbook chapter demonstrates how to use it.
10+
11+Extensions and imports:
12+```haskell
13+{-# LANGUAGE DataKinds #-}
14+{-# LANGUAGE DeriveGeneric #-}
15+{-# LANGUAGE OverloadedStrings #-}
16+{-# LANGUAGE PolyKinds #-}
17+{-# LANGUAGE TypeFamilies #-}
18+{-# LANGUAGE TypeOperators #-}
19+
20+import Data.Aeson
21+import Data.Proxy
22+import Data.Text
23+import GHC.Generics
24+import Network.Wai
25+import Network.Wai.Handler.Warp
26+
27+import Servant
28+
29+import Data.String.Conversions
30+ (cs)
31+import Servant.API.ContentTypes
32+```
33+
34+The API (from `greet.hs` example in Servant sources):
35+
36+```haskell
37+-- | A greet message data type
38+newtype Greet = Greet { _msg :: Text }
39+ deriving (Generic, Show)
40+
41+instance FromJSON Greet
42+instance ToJSON Greet
43+
44+-- API specification
45+type TestApi =
46+ -- GET /hello/:name?capital={true, false} returns a Greet as JSON
47+ "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON] Greet
48+
49+ -- POST /greet with a Greet as JSON in the request body,
50+ -- returns a Greet as JSON
51+ :<|> "greet" :> ReqBody '[JSON] Greet :> Post '[JSON] Greet
52+
53+ -- DELETE /greet/:greetid
54+ :<|> "greet" :> Capture "greetid" Text :> Delete '[JSON] NoContent
55+
56+testApi :: Proxy TestApi
57+testApi = Proxy
58+
59+-- Server-side handlers.
60+--
61+-- There's one handler per endpoint, which, just like in the type
62+-- that represents the API, are glued together using :<|>.
63+--
64+-- Each handler runs in the 'Handler' monad.
65+server :: Server TestApi
66+server = helloH :<|> postGreetH :<|> deleteGreetH
67+
68+ where helloH name Nothing = helloH name (Just False)
69+ helloH name (Just False) = return . Greet $ "Hello, " <> name
70+ helloH name (Just True) = return . Greet . toUpper $ "Hello, " <> name
71+
72+ postGreetH greet = return greet
73+
74+ deleteGreetH _ = return NoContent
75+```
76+
77+## Error formatters
78+
79+`servant-server` provides an `ErrorFormatter` type to specify how the error message will be
80+formatted. A formatter is just a function accepting three parameters:
81+
82+- `TypeRep` from `Data.Typeable`: this is a runtime representation of the type of the combinator
83+ (like `Capture` or `ReqBody`) that generated the error. It can be used to display its name (with
84+ `show`) or even dynamically dispatch on the combinator type. See the docs for `Data.Typeable` and
85+ `Type.Reflection` modules.
86+- `Request`: full information for the request that led to the error.
87+- `String`: specific error message from the combinator.
88+
89+The formatter is expected to produce a `ServerError` which will be returned from the handler.
90+
91+Additionally, there is `NotFoundErrorFormatter`, which accepts only `Request` and can customize the
92+error in case when no route can be matched (HTTP 404).
93+
94+Let's make two formatters. First one will wrap our error in a JSON:
95+
96+```json
97+{
98+ "error": "ERROR MESSAGE",
99+ "combinator": "NAME OF THE COMBINATOR"
100+}
101+```
102+
103+Additionally, this formatter will examine the `Accept` header of the request and generate JSON
104+message only if client can accept it.
105+
106+```haskell
107+customFormatter :: ErrorFormatter
108+customFormatter tr req err =
109+ let
110+ -- aeson Value which will be sent to the client
111+ value = object ["combinator" .= show tr, "error" .= err]
112+ -- Accept header of the request
113+ accH = getAcceptHeader req
114+ in
115+ -- handleAcceptH is Servant's function that checks whether the client can accept a
116+ -- certain message type.
117+ -- In this case we call it with "Proxy '[JSON]" argument, meaning that we want to return a JSON.
118+ case handleAcceptH (Proxy :: Proxy '[JSON]) accH value of
119+ -- If client can't handle JSON, we just return the body the old way
120+ Nothing -> err400 { errBody = cs err }
121+ -- Otherwise, we return the JSON formatted body and set the "Content-Type" header.
122+ Just (ctypeH, body) -> err400
123+ { errBody = body
124+ , errHeaders = [("Content-Type", cs ctypeH)]
125+ }
126+
127+notFoundFormatter :: NotFoundErrorFormatter
128+notFoundFormatter req =
129+ err404 { errBody = cs $ "Not found path: " <> rawPathInfo req }
130+```
131+
132+If you don't need to react to the `Accept` header, you can just unconditionally return the JSON like
133+this (with `encode` from `Data.Aeson`):
134+
135+```
136+err400
137+ { errBody = encode body
138+ , errHeaders = [("Content-Type", "application/json")]
139+ }
140+```
141+
142+## Passing formatters to Servant
143+
144+Servant uses the Context to configure formatters. You only need to add a value of type
145+`ErrorFormatters` to your context. This is a record with the following fields:
146+
147+- `bodyParserErrorFormatter :: ErrorFormatter`
148+- `urlParseErrorFormatter :: ErrorFormatter`
149+- `headerParseErrorFormatter :: ErrorFormatter`
150+- `notFoundErrorFormatter :: NotFoundErrorFormatter`
151+
152+Default formatters are exported as `defaultErrorFormatters`, so you can use record update syntax to
153+set the only ones you need:
154+
155+```haskell
156+customFormatters :: ErrorFormatters
157+customFormatters = defaultErrorFormatters
158+ { bodyParserErrorFormatter = customFormatter
159+ , notFoundErrorFormatter = notFoundFormatter
160+ }
161+```
162+
163+And at last, use `serveWithContext` to run your server as usual:
164+
165+```haskell
166+app :: Application
167+app = serveWithContext testApi (customFormatters :. EmptyContext) server
168+
169+main :: IO ()
170+main = run 8000 app
171+```
172+
173+Now if we try to request something with a wrong body, we will get a nice error:
174+
175+```
176+$ http -j POST localhost:8000/greet 'foo=bar'
177+HTTP/1.1 400 Bad Request
178+Content-Type: application/json;charset=utf-8
179+Date: Fri, 17 Jul 2020 13:34:18 GMT
180+Server: Warp/3.3.12
181+Transfer-Encoding: chunked
182+
183+{
184+ "combinator": "ReqBody'",
185+ "error": "Error in $: parsing Main.Greet(Greet) failed, key \"_msg\" not found"
186+}
187+```
188+
189+Notice the `Content-Type` header set by our combinator.