When using servant, I'd like to return all errors as JSON. Currently, if a request fails to parse, I see an error message like this, returned as plain text
Failed reading: not a valid json value
Instead I would like to return this as application/json
{"error":"Failed reading: not a valid json value"}
How can I do this? The docs say ServantErr
is the default error type, and I can certainly respond with custom errors inside my handlers, but if parsing fails I don't see how I can return a custom error.
Taking inspiration from @codedmart I also use a middleware, but it does not construct the json, it only changes the content type of the response when there is an error, and keep the original error message.
The json is built beforehand with a function overriding the Servant throwError function.
then I can throw any error with a custom message, it will be served as a json with the correct content-type :
Currently right now I just handle this in middleware. I do something like the following:
And then I can use just like any other middleware:
First, some language extensions
Now then
Unfortunately this is more difficult than it should be. Servant, while well-designed and the composition of small logical parts, is very opinionated about how HTTP services should operate. The default implementation of
ReqBody
, which you are probably using, is hard-coded to spit out a text string.However, we can switch out
ReqBody
for our own data type:In this very brief amount of code a lot is happening:
We are teaching the
servant-server
package on how to handle our new datatype when it appears in the type resolution forserve (Proxy :: Proxy (Body foo :> bar)) server
.We have ripped most of the code from the v0.8.1 release of
ReqBody
.We are adding a function to the pipeline that processes request bodies.
In it, we attempt to decode to the
a
parameter ofBody
. On failure, we spit out a JSON blob and an HTTP 400.We are entirely ignoring content-type headers here, for brevity.
Here is the type of the JSON blob:
Most of this machinery is internal to
servant-server
and underdocumented and rather fragile. For example, already I see that the code diverges onmaster
branch and the arity of myaddBodyCheck
has changed.Though the Servant project is still quite young and remarkably ambitious, I have to say that the aesthetics and robustness of this solution are definitely underwhelming.
To test this
We will need a Main module:
And a shell:
Ta-da!
You should be able to run all this code on LTS-7.16.
What did we learn
(1) Servant and Haskell are fun.
(2) The typeclass machinery of Servant allows for a kind of plug-and-play when it comes to the types you specify in your API. We can take out
ReqBody
and replace it with our own; on a project I did at work we even replaced the Servant verbs (GET
,POST
, ...) with our own. We wrote new content types and we even did something similar withReqBody
like you saw here.(3) It is the remarkable ability of the GHC compiler that we can destructure types during compile-time to influence runtime behavior in a safe and logically sound way. That we can express a tree of API routes at the type-level and then walk over them using typeclass instances, accumulating a server type using type families, is a wonderfully elegant way to build a well-typed web service.