Accept multiple content-types in client.

When defining a route like the following,

```
type API = Get '[JSON, PlainText] Int
```

servant-client is only able to receive responses with the content type
that comes first in the list.  In this example, it will only be able to
receive `application/json` responses.

This PR changes it so that servant-client will accept _any_ content-type
in the list.

In the example above, it will accept responses with a content type of
`application/json` or `text/plain`.
This commit is contained in:
(cdep)illabout 2016-07-14 15:54:46 +09:00
parent 197ed0548a
commit 2b737e4c1c
2 changed files with 30 additions and 18 deletions

View file

@ -39,6 +39,7 @@ import Network.HTTP.Media
import qualified Network.HTTP.Types as H
import qualified Network.HTTP.Types.Header as HTTP
import Servant.API
import Servant.API.ContentTypes
import Servant.Client.Experimental.Auth
import Servant.Common.BaseUrl
import Servant.Common.BasicAuth
@ -152,11 +153,11 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient sublayout)
instance OVERLAPPABLE_
-- Note [Non-Empty Content Types]
(MimeUnrender ct a, ReflectMethod method, cts' ~ (ct ': cts)
) => HasClient (Verb method status cts' a) where
type Client (Verb method status cts' a) = Manager -> BaseUrl -> ClientM a
( AllMime cts, AllMimeUnrender cts a, ReflectMethod method
) => HasClient (Verb method status cts a) where
type Client (Verb method status cts a) = Manager -> BaseUrl -> ClientM a
clientWithRoute Proxy req manager baseurl =
snd <$> performRequestCT (Proxy :: Proxy ct) method req manager baseurl
snd <$> performRequestCT (Proxy :: Proxy cts) method req manager baseurl
where method = reflectMethod (Proxy :: Proxy method)
instance OVERLAPPING_
@ -169,13 +170,13 @@ instance OVERLAPPING_
instance OVERLAPPING_
-- Note [Non-Empty Content Types]
( MimeUnrender ct a, BuildHeadersTo ls, ReflectMethod method, cts' ~ (ct ': cts)
) => HasClient (Verb method status cts' (Headers ls a)) where
type Client (Verb method status cts' (Headers ls a))
( AllMime cts, AllMimeUnrender cts a, BuildHeadersTo ls, ReflectMethod method
) => HasClient (Verb method status cts (Headers ls a)) where
type Client (Verb method status cts (Headers ls a))
= Manager -> BaseUrl -> ClientM (Headers ls a)
clientWithRoute Proxy req manager baseurl = do
let method = reflectMethod (Proxy :: Proxy method)
(hdrs, resp) <- performRequestCT (Proxy :: Proxy ct) method req manager baseurl
(hdrs, resp) <- performRequestCT (Proxy :: Proxy cts) method req manager baseurl
return $ Headers { getResponse = resp
, getHeadersHList = buildHeadersTo hdrs
}

View file

@ -12,7 +12,7 @@ import Control.Monad
import Control.Monad.Catch (MonadThrow)
import Control.Monad.IO.Class
import Control.Monad.Trans.Except
import Data.ByteString.Lazy hiding (pack, filter, map, null, elem)
import Data.ByteString.Lazy hiding (any, foldr, pack, filter, map, null, elem)
import Data.String
import Data.String.Conversions
import Data.Proxy
@ -178,17 +178,28 @@ performRequest reqMethod req manager reqHost = do
throwE $ FailureResponse status ct body
return (status_code, body, ct, hdrs, response)
performRequestCT :: MimeUnrender ct result =>
Proxy ct -> Method -> Req -> Manager -> BaseUrl
performRequestCT :: (AllMime cts, AllMimeUnrender cts result) =>
Proxy cts -> Method -> Req -> Manager -> BaseUrl
-> ClientM ([HTTP.Header], result)
performRequestCT ct reqMethod req manager reqHost = do
let acceptCT = contentType ct
performRequestCT cts reqMethod req manager reqHost = do
let acceptCTs = allMime cts
(_status, respBody, respCT, hdrs, _response) <-
performRequest reqMethod (req { reqAccept = [acceptCT] }) manager reqHost
unless (matches respCT (acceptCT)) $ throwE $ UnsupportedContentType respCT respBody
case mimeUnrender ct respBody of
Left err -> throwE $ DecodeFailure err respCT respBody
Right val -> return (hdrs, val)
performRequest reqMethod (req { reqAccept = acceptCTs }) manager reqHost
unless (any (matches respCT) acceptCTs) $ throwE $ UnsupportedContentType respCT respBody
let unrenderedResp = allMimeUnrender cts respBody
firstSuccess = firstPred (either (const Nothing) Just) $ snd <$> unrenderedResp
case firstSuccess of
Nothing ->
let firstErr = firstPred (either Just (const Nothing)) $ snd <$> unrenderedResp
in case firstErr of
Nothing -> throwE $ DecodeFailure "failed to find the content-type render failure" respCT respBody
Just err -> throwE $ DecodeFailure err respCT respBody
Just val -> pure (hdrs, val)
where
-- In @firstPred f eitherList@, fold over a list of 'Either', returning the
-- first 'Just' value generated by the function @f@.
firstPred :: (Either e x -> Maybe a) -> [Either e x] -> Maybe a
firstPred f = foldr (\eith accum -> maybe accum Just $ f eith) Nothing
performRequestNoBody :: Method -> Req -> Manager -> BaseUrl
-> ClientM [HTTP.Header]