servant/servant-client/src/Servant/Client/Internal/HttpClient.hs

258 lines
8.9 KiB
Haskell
Raw Normal View History

2017-08-28 18:36:05 +02:00
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
2017-09-12 18:38:52 +02:00
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
2017-08-28 18:36:05 +02:00
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
2018-03-23 17:36:24 +01:00
{-# LANGUAGE RankNTypes #-}
2017-08-28 18:36:05 +02:00
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
2017-09-12 18:38:52 +02:00
module Servant.Client.Internal.HttpClient where
2017-08-28 18:36:05 +02:00
2018-06-29 21:08:26 +02:00
import Prelude ()
import Prelude.Compat
import Control.Concurrent.STM.TVar
import Control.Exception
import Control.Monad
2018-06-29 21:08:26 +02:00
import Control.Monad.Base
(MonadBase (..))
import Control.Monad.Catch
(MonadCatch, MonadThrow)
import Control.Monad.Codensity
(Codensity (..))
2018-06-29 21:08:26 +02:00
import Control.Monad.Error.Class
(MonadError (..))
2017-09-08 01:11:20 +02:00
import Control.Monad.Reader
2018-06-29 21:08:26 +02:00
import Control.Monad.STM
(atomically)
import Control.Monad.Trans.Control
(MonadBaseControl (..))
2017-09-08 01:11:20 +02:00
import Control.Monad.Trans.Except
2018-06-29 21:08:26 +02:00
import Data.ByteString.Builder
(toLazyByteString)
2017-09-08 01:07:18 +02:00
import qualified Data.ByteString.Lazy as BSL
2018-06-29 21:08:26 +02:00
import Data.Foldable
(for_, toList)
import Data.Functor.Alt
(Alt (..))
import Data.Maybe
(maybeToList)
import Data.Proxy
(Proxy (..))
import Data.Semigroup
((<>))
import Data.Sequence
(fromList)
import Data.String
(fromString)
2017-09-08 01:07:18 +02:00
import qualified Data.Text as T
2018-06-29 21:08:26 +02:00
import Data.Time.Clock
(getCurrentTime)
import GHC.Generics
2018-06-29 21:08:26 +02:00
import Network.HTTP.Media
(renderHeader)
import Network.HTTP.Types
(hContentType, renderQuery, statusCode)
2017-09-08 01:07:18 +02:00
import Servant.Client.Core
import qualified Network.HTTP.Client as Client
2017-08-28 18:36:05 +02:00
2017-09-13 17:05:48 +02:00
-- | The environment in which a request is run.
2017-08-28 18:36:05 +02:00
data ClientEnv
= ClientEnv
2017-09-08 01:07:18 +02:00
{ manager :: Client.Manager
2017-08-28 18:36:05 +02:00
, baseUrl :: BaseUrl
, cookieJar :: Maybe (TVar Client.CookieJar)
2017-08-28 18:36:05 +02:00
}
-- | 'ClientEnv' smart constructor.
mkClientEnv :: Client.Manager -> BaseUrl -> ClientEnv
mkClientEnv mgr burl = ClientEnv mgr burl Nothing
2017-09-13 17:05:48 +02:00
-- | Generates a set of client functions for an API.
--
-- Example:
--
-- > type API = Capture "no" Int :> Get '[JSON] Int
-- > :<|> Get '[JSON] [Bool]
-- >
-- > api :: Proxy API
-- > api = Proxy
-- >
-- > getInt :: Int -> ClientM Int
-- > getBools :: ClientM [Bool]
-- > getInt :<|> getBools = client api
2017-09-12 18:38:52 +02:00
client :: HasClient ClientM api => Proxy api -> Client ClientM api
client api = api `clientIn` (Proxy :: Proxy ClientM)
2018-03-23 17:36:24 +01:00
-- | Change the monad the client functions live in, by
-- supplying a conversion function
-- (a natural transformation to be precise).
--
-- For example, assuming you have some @manager :: 'Manager'@ and
-- @baseurl :: 'BaseUrl'@ around:
--
-- > type API = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int
-- > api :: Proxy API
-- > api = Proxy
-- > getInt :: IO Int
-- > postInt :: Int -> IO Int
-- > getInt :<|> postInt = hoistClient api (flip runClientM cenv) (client api)
-- > where cenv = mkClientEnv manager baseurl
2018-03-23 17:36:24 +01:00
hoistClient
:: HasClient ClientM api
=> Proxy api
-> (forall a. m a -> n a)
-> Client m api
-> Client n api
hoistClient = hoistClientMonad (Proxy :: Proxy ClientM)
2017-08-28 18:36:05 +02:00
-- | @ClientM@ is the monad in which client functions run. Contains the
2017-09-13 22:01:31 +02:00
-- 'Client.Manager' and 'BaseUrl' used for requests in the reader environment.
2017-09-13 17:05:48 +02:00
newtype ClientM a = ClientM
{ unClientM :: ReaderT ClientEnv (ExceptT ServantError IO) a }
2017-09-13 17:05:48 +02:00
deriving ( Functor, Applicative, Monad, MonadIO, Generic
, MonadReader ClientEnv, MonadError ServantError, MonadThrow
, MonadCatch)
2017-08-28 18:36:05 +02:00
instance MonadBase IO ClientM where
liftBase = ClientM . liftBase
instance MonadBaseControl IO ClientM where
type StM ClientM a = Either ServantError a
liftBaseWith f = ClientM (liftBaseWith (\g -> f (g . unClientM)))
2017-08-28 18:36:05 +02:00
restoreM st = ClientM (restoreM st)
-- | Try clients in order, last error is preserved.
instance Alt ClientM where
a <!> b = a `catchError` \_ -> b
2017-09-08 01:07:18 +02:00
instance RunClient ClientM where
runRequest = performRequest
2017-10-25 02:12:21 +02:00
streamingRequest = performStreamingRequest
2017-09-15 20:57:03 +02:00
throwServantError = throwError
2017-09-08 01:07:18 +02:00
2017-09-12 20:43:16 +02:00
instance ClientLike (ClientM a) (ClientM a) where
mkClient = id
2017-08-28 18:36:05 +02:00
runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
2017-08-28 18:36:05 +02:00
withClientM
:: ClientM (Codensity IO a) -- ^ client with codensity result
-> ClientEnv -- ^ environment
-> (Either ServantError a -> IO b) -- ^ continuation
-> IO b
withClientM cm env k = do
e <- runExceptT (runReaderT (unClientM cm) env)
case e of
Left err -> k (Left err)
Right cod -> runCodensity cod (k . Right)
2017-09-08 01:07:18 +02:00
performRequest :: Request -> ClientM Response
performRequest req = do
ClientEnv m burl cookieJar' <- ask
let clientRequest = requestToClientRequest burl req
request <- case cookieJar' of
Nothing -> pure clientRequest
Just cj -> liftIO $ do
now <- getCurrentTime
atomically $ do
oldCookieJar <- readTVar cj
let (newRequest, newCookieJar) =
Client.insertCookiesIntoRequest
(requestToClientRequest burl req)
oldCookieJar
now
writeTVar cj newCookieJar
pure newRequest
2017-08-28 18:36:05 +02:00
eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request m
case eResponse of
Left err -> throwError err
2017-08-28 18:36:05 +02:00
Right response -> do
for_ cookieJar' $ \cj -> liftIO $ do
now' <- getCurrentTime
atomically $ modifyTVar' cj (fst . Client.updateCookieJar response request now')
2017-08-28 18:36:05 +02:00
let status = Client.responseStatus response
status_code = statusCode status
ourResponse = clientResponseToResponse response
2017-08-28 18:36:05 +02:00
unless (status_code >= 200 && status_code < 300) $
2017-09-08 01:07:18 +02:00
throwError $ FailureResponse ourResponse
return ourResponse
2017-10-25 02:12:21 +02:00
performStreamingRequest :: Request -> ClientM StreamingResponse
performStreamingRequest req = do
m <- asks manager
burl <- asks baseUrl
let request = requestToClientRequest burl req
return $ Codensity $
2017-10-25 02:12:21 +02:00
\k -> Client.withResponse request m $
\r -> do
let status = Client.responseStatus r
status_code = statusCode status
unless (status_code >= 200 && status_code < 300) $ do
b <- BSL.fromChunks <$> Client.brConsume (Client.responseBody r)
throw $ FailureResponse $ clientResponseToResponse r { Client.responseBody = b }
k (clientResponseToResponse r)
2017-10-25 02:12:21 +02:00
clientResponseToResponse :: Client.Response a -> GenResponse a
clientResponseToResponse r = Response
2017-09-08 01:07:18 +02:00
{ responseStatusCode = Client.responseStatus r
, responseBody = Client.responseBody r
, responseHeaders = fromList $ Client.responseHeaders r
, responseHttpVersion = Client.responseVersion r
}
requestToClientRequest :: BaseUrl -> Request -> Client.Request
requestToClientRequest burl r = Client.defaultRequest
{ Client.method = requestMethod r
, Client.host = fromString $ baseUrlHost burl
, Client.port = baseUrlPort burl
, Client.path = BSL.toStrict
$ fromString (baseUrlPath burl)
<> toLazyByteString (requestPath r)
, Client.queryString = renderQuery True . toList $ requestQueryString r
, Client.requestHeaders =
maybeToList acceptHdr ++ maybeToList contentTypeHdr ++ headers
2017-09-08 01:07:18 +02:00
, Client.requestBody = body
2017-09-29 01:15:53 +02:00
, Client.secure = isSecure
2017-09-08 01:07:18 +02:00
}
where
-- Content-Type and Accept are specified by requestBody and requestAccept
headers = filter (\(h, _) -> h /= "Accept" && h /= "Content-Type") $
toList $requestHeaders r
acceptHdr
| null hs = Nothing
| otherwise = Just ("Accept", renderHeader hs)
where
hs = toList $ requestAccept r
convertBody bd = case bd of
RequestBodyLBS body' -> Client.RequestBodyLBS body'
RequestBodyBS body' -> Client.RequestBodyBS body'
RequestBodyBuilder size body' -> Client.RequestBodyBuilder size body'
RequestBodyStream size body' -> Client.RequestBodyStream size body'
RequestBodyStreamChunked body' -> Client.RequestBodyStreamChunked body'
RequestBodyIO body' -> Client.RequestBodyIO (convertBody <$> body')
2017-09-08 01:07:18 +02:00
(body, contentTypeHdr) = case requestBody r of
Nothing -> (Client.RequestBodyLBS "", Nothing)
Just (body', typ)
-> (convertBody body', Just (hContentType, renderHeader typ))
2017-09-29 01:15:53 +02:00
isSecure = case baseUrlScheme burl of
Http -> False
Https -> True
2017-08-28 18:36:05 +02:00
catchConnectionError :: IO a -> IO (Either ServantError a)
catchConnectionError action =
catch (Right <$> action) $ \e ->
2017-09-08 01:07:18 +02:00
pure . Left . ConnectionError . T.pack $ show (e :: Client.HttpException)