packages feed

freckle-app-1.15.1.0: library/Freckle/App/Http/Cache/Gzip.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoFieldSelectors #-}

-- | Type and functions for handling gzipped HTTP responses
--
-- In order to optimize caching of responses in storage with size limitations,
-- we cache gzipped responses as-is. This requires disabling the automatic
-- decompression of @http-client@ and handling it ourselves.
--
-- The module makes that a type-enforced process:
--
-- - 'requestPotentiallyGzipped' is the only way to get a 'PotentiallyGzipped'
-- - Which is the type needed for the response field in 'CachedResponse'
-- - 'gunzipResponseBody' is the only way to erase 'PotentiallyGzipped'
-- - Which is what you actually need to return
module Freckle.App.Http.Cache.Gzip
  ( PotentiallyGzipped
  , requestPotentiallyGzipped
  , gunzipResponseBody
  ) where

import Freckle.App.Prelude

import Codec.Serialise (Serialise)
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BSL
import Freckle.App.Http (disableRequestDecompress)
import Freckle.App.Http.Header
import Network.HTTP.Client (Request, Response)
import qualified Network.HTTP.Client.Internal as HTTP

newtype PotentiallyGzipped a = PotentiallyGzipped
  { unwrap :: a
  }
  deriving stock (Show, Eq)
  deriving newtype (Serialise)

-- | Run a request /without/ automatic 'decompress' and tag the @body@ type
requestPotentiallyGzipped
  :: Functor m
  => (Request -> m (Response body))
  -> Request
  -> m (Response (PotentiallyGzipped body))
requestPotentiallyGzipped doHttp =
  fmap (fmap PotentiallyGzipped) . doHttp . disableRequestDecompress

-- | Gunzip a 'PotentiallyGzipped' body, if necessary
gunzipResponseBody
  :: MonadIO m
  => Request
  -> Response (PotentiallyGzipped ByteString)
  -> m (Response ByteString)
gunzipResponseBody req resp
  | HTTP.needsGunzip req (getHeaders resp) = liftIO $ do
      body <- gunzipBody $ HTTP.responseBody resp
      pure $ body <$ resp
  | otherwise = pure $ (.unwrap) <$> resp

gunzipBody :: PotentiallyGzipped ByteString -> IO ByteString
gunzipBody body = do
  body1 <- HTTP.constBodyReader $ BSL.toChunks body.unwrap
  reader <- HTTP.makeGzipReader body1
  BSL.fromChunks <$> HTTP.brConsume reader