mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 21:12:09 +03:00
231 lines
8.3 KiB
Haskell
231 lines
8.3 KiB
Haskell
|
module Network.HTTP.Client.Transformable
|
||
|
( Request
|
||
|
, mkRequestThrow
|
||
|
, mkRequestEither
|
||
|
, url
|
||
|
, Network.HTTP.Client.Transformable.method
|
||
|
, headers
|
||
|
, host
|
||
|
, body
|
||
|
, port
|
||
|
, path
|
||
|
, queryParams
|
||
|
, timeout
|
||
|
, getReqSize
|
||
|
, getQueryStr
|
||
|
, performRequest
|
||
|
, Client.Response(..)
|
||
|
, Client.ResponseTimeout
|
||
|
, Client.HttpException(..)
|
||
|
, Internal.HttpExceptionContent(..)
|
||
|
, Client.Manager
|
||
|
, Client.responseTimeoutDefault
|
||
|
, Client.responseTimeoutMicro
|
||
|
, Client.newManager
|
||
|
, module Types
|
||
|
, module TLSClient
|
||
|
) where
|
||
|
|
||
|
import Prelude
|
||
|
|
||
|
import Control.Exception (throw)
|
||
|
import Control.Lens (Lens', lens, set, view)
|
||
|
import Control.Monad.Catch (MonadThrow, fromException)
|
||
|
import Data.Bifunctor (bimap)
|
||
|
import Data.ByteString (ByteString)
|
||
|
import Data.Function ((&))
|
||
|
import Data.Int (Int64)
|
||
|
import Data.Maybe (fromMaybe)
|
||
|
import Network.HTTP.Client.TLS as TLSClient
|
||
|
import Network.HTTP.Types as Types
|
||
|
|
||
|
import qualified Data.Aeson as J
|
||
|
import qualified Data.ByteString as B
|
||
|
import qualified Data.ByteString.Char8 as C8
|
||
|
import qualified Data.ByteString.Lazy as BL
|
||
|
import qualified Data.CaseInsensitive as CI
|
||
|
import qualified Data.Text as T
|
||
|
import qualified Data.Text.Encoding as TE
|
||
|
import qualified Network.HTTP.Client as Client
|
||
|
import qualified Network.HTTP.Client.Internal as Internal
|
||
|
import qualified Network.HTTP.Conduit as NHS
|
||
|
import qualified Network.HTTP.Simple as NHS
|
||
|
import qualified Network.URI as URI
|
||
|
|
||
|
-- | @Network.HTTP.Client@.'Client.Request' stores the request body in a sum
|
||
|
-- type which has a case containing IO along with some other unwieldy cases.
|
||
|
-- This makes it difficult to log our requests before and after transformation.
|
||
|
--
|
||
|
-- In our codebase we only ever use the Lazy ByteString case. So by
|
||
|
-- lifting the request body out of Network.HTTP.Client.Request, we
|
||
|
-- make it much easier to log our Requests.
|
||
|
--
|
||
|
-- When executing the request we simply insert the value at `rdBody`
|
||
|
-- into the Request.
|
||
|
--
|
||
|
-- When working with Transformable Requests you should always import
|
||
|
-- this module qualified and use the `mkRequest*` functions for
|
||
|
-- constructing requests. Modification of Request should be done using
|
||
|
-- the provided lens API.
|
||
|
--
|
||
|
-- NOTE: This module is meant to be imported qualified, e.g.
|
||
|
--
|
||
|
-- > import qualified Network.HTTP.Client.Transformable as HTTP
|
||
|
--
|
||
|
-- ...or
|
||
|
--
|
||
|
-- > import qualified Network.HTTP.Client.Transformable as Transformable
|
||
|
data Request = Request
|
||
|
{ rdRequest :: Client.Request
|
||
|
, rdBody :: Maybe BL.ByteString
|
||
|
} deriving Show
|
||
|
|
||
|
instance J.ToJSON Request where
|
||
|
toJSON req@Request{..} =
|
||
|
J.object
|
||
|
[ "url" J..= view url req
|
||
|
, "method" J..= TE.decodeUtf8 (view method req)
|
||
|
, "headers" J..= fmap (bimap (TE.decodeUtf8 . CI.original) TE.decodeUtf8) (view headers req)
|
||
|
, "body" J..= fmap (TE.decodeUtf8 . BL.toStrict) rdBody
|
||
|
, "query_string" J..= TE.decodeUtf8 (Client.queryString rdRequest)
|
||
|
, "response_timeout" J..= serializeRT (view timeout req)
|
||
|
]
|
||
|
where
|
||
|
serializeRT = \case
|
||
|
Internal.ResponseTimeoutMicro i -> show i
|
||
|
Internal.ResponseTimeoutNone -> "None"
|
||
|
Internal.ResponseTimeoutDefault -> "default"
|
||
|
|
||
|
-- | Convert a URL into a Request value. This function can throw
|
||
|
-- if the URL is invalid.
|
||
|
mkRequestThrow :: MonadThrow m => T.Text -> m Request
|
||
|
mkRequestThrow url' = do
|
||
|
req <- Client.parseRequest $ T.unpack url'
|
||
|
pure $ Request req Nothing
|
||
|
|
||
|
-- | A concrete version of `mkRequestThrow` in the Either monad.
|
||
|
mkRequestEither :: T.Text -> Either Client.HttpException Request
|
||
|
mkRequestEither url' =
|
||
|
case mkRequestThrow url' of
|
||
|
Right res -> Right res
|
||
|
Left e -> case fromException @Client.HttpException e of
|
||
|
Just httpExcept -> Left httpExcept
|
||
|
Nothing -> throw e
|
||
|
|
||
|
-- | Url is 'materialized view' into `Request` consisting of
|
||
|
-- concatenation of `host`, `port`, `queryParams`, and `path`.
|
||
|
--
|
||
|
-- We use `Network.HTTP.Client.getUri` to `view` the value but we must
|
||
|
-- carefully set the subcomponents by hand during `set` operations. Be
|
||
|
-- careful modifying this lens and verify against the unit tests..
|
||
|
url :: Lens' Request T.Text
|
||
|
url = lens getUrl setUrl
|
||
|
where
|
||
|
getUrl :: Request -> T.Text
|
||
|
getUrl Request{rdRequest} = T.pack $ URI.uriToString id (Client.getUri rdRequest) mempty
|
||
|
|
||
|
setUrl :: Request -> T.Text -> Request
|
||
|
setUrl req url' = fromMaybe req $ do
|
||
|
uri <- URI.parseURI (T.unpack url')
|
||
|
URI.URIAuth{..} <- URI.uriAuthority uri
|
||
|
let host' = C8.pack $ uriUserInfo <> uriRegName
|
||
|
port' = case uriPort of
|
||
|
':':newPort -> read @Int newPort
|
||
|
_ -> 80
|
||
|
queryString = Types.queryTextToQuery $ Types.parseQueryText $ C8.pack $ URI.uriQuery uri
|
||
|
path' = C8.pack $ URI.uriPath uri
|
||
|
|
||
|
pure $ req & set host host'
|
||
|
& set port port'
|
||
|
& set queryParams queryString
|
||
|
& set path path'
|
||
|
|
||
|
body :: Lens' Request (Maybe BL.ByteString)
|
||
|
body = lens rdBody setBody
|
||
|
where
|
||
|
setBody :: Request -> Maybe BL.ByteString -> Request
|
||
|
setBody req body' = req { rdBody = body' }
|
||
|
|
||
|
headers :: Lens' Request [Types.Header]
|
||
|
headers = lens getHeaders setHeaders
|
||
|
where
|
||
|
getHeaders :: Request -> [Types.Header]
|
||
|
getHeaders Request{rdRequest} = Client.requestHeaders rdRequest
|
||
|
|
||
|
setHeaders :: Request -> [Types.Header] -> Request
|
||
|
setHeaders req@Request{rdRequest} headers' =
|
||
|
req { rdRequest = NHS.setRequestHeaders headers' rdRequest }
|
||
|
|
||
|
host :: Lens' Request B.ByteString
|
||
|
host = lens getHost setHost
|
||
|
where
|
||
|
getHost :: Request -> B.ByteString
|
||
|
getHost Request{rdRequest} = Client.host rdRequest
|
||
|
|
||
|
setHost :: Request -> B.ByteString -> Request
|
||
|
setHost req@Request{rdRequest} host' =
|
||
|
req { rdRequest = NHS.setRequestHost host' rdRequest }
|
||
|
|
||
|
method :: Lens' Request B.ByteString
|
||
|
method = lens getMethod setMethod
|
||
|
where
|
||
|
getMethod :: Request -> B.ByteString
|
||
|
getMethod Request{rdRequest} = Client.method rdRequest
|
||
|
|
||
|
setMethod :: Request -> B.ByteString -> Request
|
||
|
setMethod req@Request{rdRequest} method' = req { rdRequest = NHS.setRequestMethod method' rdRequest }
|
||
|
|
||
|
path :: Lens' Request B.ByteString
|
||
|
path = lens getPath setPath
|
||
|
where
|
||
|
getPath :: Request -> B.ByteString
|
||
|
getPath Request{rdRequest} = Client.path rdRequest
|
||
|
|
||
|
setPath :: Request -> B.ByteString -> Request
|
||
|
setPath req@Request{rdRequest} p =
|
||
|
req { rdRequest = rdRequest { Client.path = p } }
|
||
|
|
||
|
port :: Lens' Request Int
|
||
|
port = lens getPort setPort
|
||
|
where
|
||
|
getPort :: Request -> Int
|
||
|
getPort Request{rdRequest} = Client.port rdRequest
|
||
|
|
||
|
setPort :: Request -> Int -> Request
|
||
|
setPort req@Request{rdRequest} i =
|
||
|
req { rdRequest = NHS.setRequestPort i rdRequest }
|
||
|
|
||
|
getQueryStr :: Request -> ByteString
|
||
|
getQueryStr = Types.renderQuery True . view queryParams
|
||
|
|
||
|
queryParams :: Lens' Request NHS.Query
|
||
|
queryParams = lens getQueryParams setQueryParams
|
||
|
where
|
||
|
getQueryParams :: Request -> NHS.Query
|
||
|
getQueryParams Request{rdRequest} = NHS.getRequestQueryString rdRequest
|
||
|
|
||
|
setQueryParams :: Request -> NHS.Query -> Request
|
||
|
setQueryParams req@Request{rdRequest} params = req { rdRequest = NHS.setQueryString params rdRequest }
|
||
|
|
||
|
timeout :: Lens' Request Client.ResponseTimeout
|
||
|
timeout = lens getTimeout setTimeout
|
||
|
where
|
||
|
getTimeout :: Request -> Client.ResponseTimeout
|
||
|
getTimeout Request{rdRequest} = Client.responseTimeout rdRequest
|
||
|
|
||
|
setTimeout :: Request -> Client.ResponseTimeout -> Request
|
||
|
setTimeout req@Request{rdRequest} timeout' =
|
||
|
let updatedReq = rdRequest { Client.responseTimeout = timeout' }
|
||
|
in req { rdRequest = updatedReq }
|
||
|
|
||
|
getReqSize :: Request -> Int64
|
||
|
getReqSize Request{rdBody} = maybe 0 BL.length rdBody
|
||
|
|
||
|
toRequest :: Request -> Client.Request
|
||
|
toRequest Request{..} = case rdBody of
|
||
|
Nothing -> rdRequest
|
||
|
Just body' -> NHS.setRequestBody (Client.RequestBodyLBS body') rdRequest
|
||
|
|
||
|
performRequest :: Request -> Client.Manager -> IO (Client.Response BL.ByteString)
|
||
|
performRequest req manager = Client.httpLbs (toRequest req) manager
|