{-# LANGUAGE TupleSections #-}
module Hreq.Client.Internal.HTTP
(
Hreq (..)
, RunClient (..)
, runHreq
, runHreqWithConfig
, runHttpClient
, checkHttpResponse
, requestToHTTPRequest
, httpResponsetoResponse
, catchConnectionError
) where
import Prelude ()
import Prelude.Compat
import Control.Concurrent.STM.TVar (TVar, modifyTVar', readTVar, writeTVar)
import Control.Monad.Catch (SomeException (..), catch, throwM)
import Control.Monad.IO.Unlift (MonadUnliftIO (..), wrappedWithRunInIO)
import Control.Monad.Reader (MonadIO (..), MonadReader, MonadTrans, ReaderT (..), ask, asks)
import Control.Monad.STM (STM, atomically)
import Control.Retry (retrying)
import qualified Data.ByteString.Lazy as LBS
import Data.Either (isLeft)
import Data.Foldable (toList)
import Data.Maybe (maybeToList)
import Data.String.Conversions (cs)
import Data.Time.Clock (UTCTime, getCurrentTime)
import GHC.Natural (Natural)
import qualified Network.HTTP.Client as HTTP
import Network.HTTP.Media (renderHeader)
import Network.HTTP.Types (Header, hAccept, hContentType, renderQuery, statusCode, statusMessage)
import Hreq.Client.Internal.Config (HttpConfig (..), StatusRange (..), createDefConfig)
import Hreq.Core.API (GivesPooper (..))
import Hreq.Core.Client (BaseUrl (..), ClientError (..), Request, RequestBody (..), RequestF (..),
Response, ResponseF (..), RunClient (..), Scheme (..))
newtype Hreq m a = Hreq { runHreq' :: ReaderT HttpConfig m a }
deriving (Functor, Applicative, Monad, MonadReader HttpConfig, MonadTrans, MonadIO)
instance MonadUnliftIO m => MonadUnliftIO (Hreq m) where
withRunInIO = wrappedWithRunInIO Hreq runHreq'
instance RunClient (Hreq IO) where
runClient = runHttpClient
throwHttpError = Hreq . throwM
checkResponse = checkHttpResponse
runHreq :: MonadIO m => BaseUrl -> Hreq m a -> m a
runHreq baseUrl action = do
config <- liftIO $ createDefConfig baseUrl
runHreqWithConfig config action
runHreqWithConfig :: HttpConfig -> Hreq m a -> m a
runHreqWithConfig config action = runReaderT (runHreq' action) config
runHttpClient
:: (MonadReader HttpConfig m, MonadIO m, RunClient m)
=> Request
-> m Response
runHttpClient req = do
config <- ask
let manager = httpManager config
let baseUrl = httpBaseUrl config
let mcookieJar = httpCookieJar config
let retryPolicy = httpRetryPolicy config
let httpRequest = requestToHTTPRequest baseUrl req
let requestAction = liftIO $ catchConnectionError
$ performHttpRequest httpRequest manager mcookieJar
ehttpResponse <- retrying retryPolicy (const (return . isLeft) ) (const requestAction)
response <- either throwHttpError (pure . httpResponsetoResponse cs) ehttpResponse
maybe (pure response) throwHttpError =<< checkResponse req response
checkHttpResponse
:: (MonadReader HttpConfig m)
=> Request
-> Response
-> m (Maybe ClientError)
checkHttpResponse req response = do
statusRange <- asks httpStatuses
let code = resStatusCode response
pure $ if code >= statusLower statusRange && code <= statusUpper statusRange
then Just $ FailureResponse req response
else Nothing
performHttpRequest
:: HTTP.Request
-> HTTP.Manager
-> Maybe (TVar HTTP.CookieJar)
-> IO (HTTP.Response LBS.ByteString)
performHttpRequest request manager mcookieJar = case mcookieJar of
Nothing -> HTTP.httpLbs request manager
Just cj -> do
req' <- cookieJarRequest cj request
HTTP.withResponseHistory req' manager $ updateWithResponseCookies cj
where
cookieJarRequest :: TVar HTTP.CookieJar -> HTTP.Request -> IO HTTP.Request
cookieJarRequest cj req = do
now <- getCurrentTime
atomically $ do
oldCookieJar <- readTVar cj
let (newReq, newCookieJar) = HTTP.insertCookiesIntoRequest req oldCookieJar now
writeTVar cj newCookieJar
pure newReq
updateWithResponseCookies
:: TVar HTTP.CookieJar
-> HTTP.HistoriedResponse HTTP.BodyReader
-> IO (HTTP.Response LBS.ByteString)
updateWithResponseCookies cj responses = do
now <- getCurrentTime
bss <- HTTP.brConsume $ HTTP.responseBody fRes
let fRes' = fRes { HTTP.responseBody = LBS.fromChunks bss }
allResponses = HTTP.hrRedirects responses <> [(fReq, fRes')]
atomically $ mapM_ (updateCookieJar now) allResponses
return fRes'
where
updateCookieJar :: UTCTime -> (HTTP.Request, HTTP.Response LBS.ByteString) -> STM ()
updateCookieJar now' (req', res') = modifyTVar' cj (fst . HTTP.updateCookieJar res' req' now')
fReq = HTTP.hrFinalRequest responses
fRes = HTTP.hrFinalResponse responses
httpResponsetoResponse :: (a -> b) -> HTTP.Response a -> ResponseF b
httpResponsetoResponse f response = Response
{ resStatusCode = statusCode $ HTTP.responseStatus response
, resStatusMsg = cs $ statusMessage $ HTTP.responseStatus response
, resHeaders = HTTP.responseHeaders response
, resBody = f $ HTTP.responseBody response
, resHttpVersion = HTTP.responseVersion response
}
requestToHTTPRequest :: BaseUrl -> Request -> HTTP.Request
requestToHTTPRequest burl r = HTTP.defaultRequest
{ HTTP.method = reqMethod r
, HTTP.host = cs $ baseUrlHost burl
, HTTP.port = fromIntegral @Natural @Int $ baseUrlPort burl
, HTTP.path = cs $ baseUrlPath burl <> reqPath r
, HTTP.queryString = renderQuery True $ toList $ reqQueryString r
, HTTP.requestHeaders = maybeToList acceptHeader <> maybeToList contentType <> headers
, HTTP.requestBody = body
, HTTP.secure = isSecure
}
where
headers :: [ Header ]
headers = filter ( \(hname, _) -> hname /= hAccept && hname /= hContentType)
$ toList $ reqHeaders r
acceptHeader :: Maybe Header
acceptHeader = (hAccept, ) . renderHeader <$> reqAccept r
(body, contentType) = case reqBody r of
Nothing -> (HTTP.RequestBodyBS mempty, Nothing)
Just (body', ctyp) ->
let addBody = (, Just (hContentType, renderHeader ctyp))
in case body' of
RequestBodyBS bs ->
addBody $ HTTP.RequestBodyBS bs
RequestBodyLBS lbs ->
addBody $ HTTP.RequestBodyLBS lbs
RequestBodyStream (GivesPooper givesPooper) ->
addBody $ HTTP.RequestBodyStreamChunked givesPooper
isSecure :: Bool
isSecure = case baseUrlScheme burl of
Http -> False
Https -> True
catchConnectionError :: IO a -> IO (Either ClientError a)
catchConnectionError action =
catch (Right <$> action)
$ \e -> pure . Left . ConnectionError $ SomeException (e :: HTTP.HttpException)