Alternative Servant Responses
Last week I had an interesting run-in with a Servant API type. I was requesting from an API that could return two possible responses. The first was 200 OK, and a JSON body. The other was 204 No Content, and, well, no content. I struggled for a while wrestling the compiler and searching in vain online for resources before finally arriving at an answer that worked for me. Below is the code!
Full example here.
Imports
Import the right stuff:
import ClassyPrelude
import Data.Aeson (FromJSON, parseJSON)
import Data.Functor.Alt ((<!>))
import Data.Proxy (Proxy (Proxy))
import Servant ((:<|>) ((:<|>)), (:>), Get, GetNoContent, JSON, NoContent, QueryParam)
import Servant.Client (ClientEnv, ClientM, client, runClientM)
import Servant.Client.Core (ClientError)
API Type
Here’s our response and API type. In my particular case it was a paging endpoint, so I’ve included
that parameter. Depending on server logic, it can either return “200 OK” and a Baz
or “204 No
Content” and no content.
data Baz = Baz
instance FromJSON Baz where
parseJSON _ = pure Baz
type BazApi = "foo" :> "bar" :> QueryParam "page" Int :> (Get '[JSON] [Baz] :<|> GetNoContent '[] NoContent)
Client Function
Here’s our function for invoking the API. It matches the type of the API itself. I typically write
these helpers so that I can erase the call to client (Proxy @MyApiType)
. Notice that we have the
monad ClientM
on either side of the :<|>
type - we will have to pattern match and then examine
which response we will end up with.
getBaz :: Maybe Int -> ClientM [Baz] :<|> ClientM NoContent
getBaz = client (Proxy @BazApi)
Using the Response
Finally, we have to figure out how to use this response. The signature above is tricky, but the
important bit here is that :<|>
is a constructor of two ClientM
values. Since there are two
possible responses and the API will return exactly one of them, we have to figure out how to unify
the two ClientM
somehow. We know that we can try to bind the first value (the Baz
) and if it
fails it should be the second value (the NoContent
). Initially I was unaware of how to do this but
while inspecting the ClientM
type in Haddock I found a clue: instance Alt ClientM
. Looking at
the definition of Alt
I found (<!>) :: f a -> f a -> f a
, which looked like what I wanted. Here
our f
is ClientM
and our a
is [Baz]
.
runGetBaz :: (MonadIO m) => ClientEnv -> Maybe Int -> m (Either ClientError [Baz])
runGetBaz env page = liftIO . flip runClientM env $ do
-- pattern match on the possible results and use the one that is successful via Alt (<!>)
case getBaz page of
someRecords :<|> noRecords -> someRecords <!> ([] <$ noRecords)
Conclusion
That’s it! The big takeaway here for me was that :<|>
isn’t just a type constructor, the API can
also return responses that are nested in the :<|>
value constructor. Servant provides the Alt
instance on ClientM
in order to bind on the possible return values.