amazonka-2.0: src/Amazonka/HTTP.hs
-- |
-- Module : Amazonka.HTTP
-- Copyright : (c) 2013-2023 Brendan Hay
-- License : Mozilla Public License, v. 2.0.
-- Maintainer : Brendan Hay <brendan.g.hay+amazonka@gmail.com>
-- Stability : provisional
-- Portability : non-portable (GHC extensions)
module Amazonka.HTTP
( retryRequest,
awaitRequest,
httpRequest,
configureRequest,
retryService,
retryStream,
)
where
import Amazonka.Core.Lens.Internal (to, (^?), _Just)
import Amazonka.Data.Body (isStreaming)
import Amazonka.Env hiding (auth)
import Amazonka.Env.Hooks (Finality (..))
import qualified Amazonka.Env.Hooks as Hooks
import Amazonka.Prelude
import Amazonka.Types
import Amazonka.Waiter
import Control.Exception as Exception
import Control.Monad.Trans.Resource (liftResourceT, transResourceT)
import qualified Control.Retry as Retry
import Data.Foldable (traverse_)
import qualified Data.Time as Time
import Data.Typeable (Typeable)
import qualified Network.HTTP.Conduit as Client.Conduit
retryRequest ::
forall m a withAuth.
( MonadResource m,
AWSRequest a,
Typeable a,
Typeable (AWSResponse a),
Foldable withAuth
) =>
Env' withAuth ->
a ->
m (Either Error (ClientResponse (AWSResponse a)))
retryRequest env@Env {hooks} rq = do
rq' <- liftIO $ Hooks.request hooks env rq
cfgRq <- configureRequest env rq'
let attempt _ = httpRequest env cfgRq
policy = retryStream cfgRq <> retryService (service cfgRq)
Request
{ service = Service {retry = Exponential {check = serviceRetryCheck}}
} = cfgRq
shouldRetry :: Retry.RetryStatus -> Either Error b -> m Bool
shouldRetry s =
liftIO . \case
Left r
| Just True <- r ^? transportErr ->
True <$ Hooks.requestRetry hooks env (cfgRq, "http_error", s)
| Just name <- r ^? serviceErr ->
True <$ Hooks.requestRetry hooks env (cfgRq, name, s)
_other -> pure False
where
transportErr =
_TransportError . to (retryCheck env (Retry.rsIterNumber s))
serviceErr =
_ServiceError . to serviceRetryCheck . _Just
Retry.retrying policy shouldRetry attempt >>= \case
Left e -> Left e <$ liftIO (Hooks.error hooks env (Final, cfgRq, e))
Right a -> pure $ Right a
awaitRequest ::
( MonadResource m,
AWSRequest a,
Typeable a,
Foldable withAuth
) =>
Env' withAuth ->
Wait a ->
a ->
m (Either Error Accept)
awaitRequest env@Env {hooks} w rq = do
rq' <- liftIO $ Hooks.request hooks env rq
cfgRq <- configureRequest env rq'
w'@Wait {..} <- liftIO $ Hooks.wait hooks env w
let handleResult res = (fromMaybe AcceptRetry $ accept w' cfgRq res, res)
attempt _ = handleResult <$> httpRequest env cfgRq
policy =
Retry.limitRetries attempts
<> Retry.constantDelay (toMicroseconds delay)
check retryStatus (a, _) = do
liftIO $ Hooks.awaitRetry hooks env (cfgRq, w', a, retryStatus)
pure $ case a of
AcceptSuccess -> False
AcceptFailure -> False
AcceptRetry -> True
Retry.retrying policy check attempt >>= \case
(AcceptSuccess, _) -> pure $ Right AcceptSuccess
(_, Left e) -> Left e <$ liftIO (Hooks.error hooks env (Final, cfgRq, e))
(a, _) -> pure $ Right a
-- | Make a one-shot request to AWS, using a configured 'Request'
-- (which contains the 'Service', plus any overrides).
httpRequest ::
( MonadResource m,
AWSRequest a,
Typeable a,
Foldable withAuth
) =>
Env' withAuth ->
Request a ->
m (Either Error (ClientResponse (AWSResponse a)))
httpRequest env@Env {hooks, manager, region} cfgRq =
liftResourceT (transResourceT (`Exception.catches` handlers) go)
where
go = do
time <- liftIO Time.getCurrentTime
clientRq :: ClientRequest <-
liftIO . Hooks.clientRequest hooks env =<< case authMaybe env of
Nothing -> pure $! requestUnsigned cfgRq region
Just auth -> withAuth auth $ \a -> do
let s@(Signed _ rq) = requestSign cfgRq a region time
liftIO $ Hooks.signedRequest hooks env s
pure $! rq
rs <- Client.Conduit.http clientRq manager
liftIO $ Hooks.clientResponse hooks env (cfgRq, void rs)
parsedRs <-
response
(Hooks.rawResponseBody hooks env)
(service cfgRq)
(proxy cfgRq)
rs
traverse_ (liftIO . Hooks.response hooks env . (cfgRq,)) parsedRs
pure parsedRs
handlers :: [Handler (Either Error b)]
handlers =
[ Handler err,
Handler $ err . TransportError
]
where
err e = Left e <$ Hooks.error hooks env (NotFinal, cfgRq, e)
proxy :: Request a -> Proxy a
proxy _ = Proxy
-- Configures an AWS request `a` into its `Request a` form, applying
-- service overrides from `env` and running hooks on the configured
-- (Request a).
configureRequest ::
(AWSRequest a, Typeable a, MonadIO m) => Env' withAuth -> a -> m (Request a)
configureRequest env@Env {overrides, hooks} =
liftIO
. Hooks.configuredRequest hooks env
. request overrides
retryStream :: Request a -> Retry.RetryPolicy
retryStream Request {body} =
Retry.RetryPolicyM $ \_ -> pure $ if isStreaming body then Nothing else Just 0
retryService :: Service -> Retry.RetryPolicy
retryService Service {retry = Exponential {..}} =
Retry.limitRetries attempts <> Retry.RetryPolicyM (pure . delay)
where
delay (Retry.rsIterNumber -> n)
| n >= 0 = Just $ truncate (grow * 1000000)
| otherwise = Nothing
where
grow = base * (fromIntegral growth ^^ (n - 1))