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

View file

@ -12,7 +12,7 @@ import Control.Monad
import Control.Monad.Catch (MonadThrow) import Control.Monad.Catch (MonadThrow)
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Except 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
import Data.String.Conversions import Data.String.Conversions
import Data.Proxy import Data.Proxy
@ -178,17 +178,28 @@ performRequest reqMethod req manager reqHost = do
throwE $ FailureResponse status ct body throwE $ FailureResponse status ct body
return (status_code, body, ct, hdrs, response) return (status_code, body, ct, hdrs, response)
performRequestCT :: MimeUnrender ct result => performRequestCT :: (AllMime cts, AllMimeUnrender cts result) =>
Proxy ct -> Method -> Req -> Manager -> BaseUrl Proxy cts -> Method -> Req -> Manager -> BaseUrl
-> ClientM ([HTTP.Header], result) -> ClientM ([HTTP.Header], result)
performRequestCT ct reqMethod req manager reqHost = do performRequestCT cts reqMethod req manager reqHost = do
let acceptCT = contentType ct let acceptCTs = allMime cts
(_status, respBody, respCT, hdrs, _response) <- (_status, respBody, respCT, hdrs, _response) <-
performRequest reqMethod (req { reqAccept = [acceptCT] }) manager reqHost performRequest reqMethod (req { reqAccept = acceptCTs }) manager reqHost
unless (matches respCT (acceptCT)) $ throwE $ UnsupportedContentType respCT respBody unless (any (matches respCT) acceptCTs) $ throwE $ UnsupportedContentType respCT respBody
case mimeUnrender ct respBody of let unrenderedResp = allMimeUnrender cts respBody
Left err -> throwE $ DecodeFailure err respCT respBody firstSuccess = firstPred (either (const Nothing) Just) $ snd <$> unrenderedResp
Right val -> return (hdrs, val) 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 performRequestNoBody :: Method -> Req -> Manager -> BaseUrl
-> ClientM [HTTP.Header] -> ClientM [HTTP.Header]