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

203 lines
7.5 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 #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
2017-08-28 18:36:05 +02:00
2017-09-13 18:36:20 +02:00
-- | @http-client@-based client requests executor
2017-09-12 18:38:52 +02:00
module Servant.Client.Internal.HttpClient where
2017-08-28 18:36:05 +02:00
import Prelude ()
import Prelude.Compat
import Control.Concurrent.STM.TVar
import Control.Exception
import Control.Monad
import Control.Monad.Base (MonadBase (..))
import Control.Monad.Catch (MonadCatch, MonadThrow)
import Control.Monad.Error.Class (MonadError (..))
2017-09-08 01:11:20 +02:00
import Control.Monad.Reader
import Control.Monad.STM (atomically)
2017-09-08 01:11:20 +02:00
import Control.Monad.Trans.Control (MonadBaseControl (..))
import Control.Monad.Trans.Except
2017-09-08 01:07:18 +02:00
import Data.ByteString.Builder (toLazyByteString)
import qualified Data.ByteString.Lazy as BSL
import Data.Foldable (toList, for_)
2017-09-08 01:11:20 +02:00
import Data.Functor.Alt (Alt (..))
import Data.Maybe (maybeToList)
2017-09-08 01:07:18 +02:00
import Data.Monoid ((<>))
2017-09-12 18:38:52 +02:00
import Data.Proxy (Proxy (..))
import Data.Sequence (fromList)
2017-09-08 01:07:18 +02:00
import Data.String (fromString)
import qualified Data.Text as T
import Data.Time.Clock (getCurrentTime)
import GHC.Generics
2017-09-08 01:11:20 +02:00
import Network.HTTP.Media (renderHeader)
2017-09-08 01:07:18 +02:00
import Network.HTTP.Types (hContentType, renderQuery,
statusCode)
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)
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
catchServantError = catchError
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
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 $ StreamingResponse $
\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 $ Response status b (fromList $ Client.responseHeaders r) (Client.responseVersion r)
k (status, fromList $ Client.responseHeaders r, Client.responseVersion r, Client.responseBody r)
2017-10-25 02:12:21 +02:00
clientResponseToResponse :: Client.Response BSL.ByteString -> Response
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
2017-09-08 01:07:18 +02:00
(body, contentTypeHdr) = case requestBody r of
Nothing -> (Client.RequestBodyLBS "", Nothing)
2017-09-08 01:11:20 +02:00
Just (RequestBodyLBS body', typ)
-> (Client.RequestBodyLBS 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)