1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE DeriveFunctor #-}
3 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE Rank2Types #-}
6 {-# LANGUAGE StrictData #-}
7 {-# LANGUAGE TypeFamilies #-}
8 {-# LANGUAGE TypeApplications #-}
9 {-# OPTIONS_GHC -fno-warn-orphans #-}
10 module Symantic.HTTP.Client.Connection where
12 import Control.Applicative (Applicative(..), Alternative(..))
13 import Control.Monad (Monad(..), unless)
14 import Control.Monad.Trans.Class (MonadTrans(..))
16 import Data.Default.Class (Default(..))
17 import Data.Either (Either(..))
18 import Data.Eq (Eq(..))
19 import Data.Foldable (null, for_, toList)
20 import Data.Function (($), (.), on)
21 import Data.Functor (Functor, (<$>))
22 import Data.Maybe (Maybe(..), maybe, fromMaybe)
23 import Data.Ord (Ord(..))
24 import Data.Proxy (Proxy(..))
25 import Data.Semigroup (Semigroup(..))
26 import Data.Sequence (Seq)
27 import Data.String (IsString(..))
28 import Data.Text (Text)
29 import Data.Tuple (fst)
31 import Text.Read (readMaybe)
32 import Text.Show (Show(..))
33 import qualified Control.Concurrent.STM as STM
34 import qualified Control.Exception as Exn
35 import qualified Control.Monad.Classes as MC
36 import qualified Control.Monad.Trans.Except as E
37 import qualified Control.Monad.Trans.Reader as R
38 import qualified Data.ByteString as BS
39 import qualified Data.ByteString.Builder as BSB
40 import qualified Data.ByteString.Lazy as BSL
41 import qualified Data.List as List
42 import qualified Data.Sequence as Seq
43 import qualified Data.Text as T
44 import qualified Data.Time.Clock as Time
45 import qualified Network.HTTP.Client as Client
46 import qualified Network.HTTP.Media as Media
47 import qualified Network.HTTP.Types as HTTP
48 import qualified Network.URI as URI
50 import Symantic.HTTP.Mime
51 import Symantic.HTTP.URI
53 -- * Type 'ClientConnection'
54 -- [ A monadic connection for a client query a server.
55 newtype ClientConnection a = ClientConnection { unClientConnection ::
57 (E.ExceptT ClientError IO)
59 } deriving (Functor, Applicative, Monad)
60 type instance MC.CanDo ClientConnection (MC.EffReader ClientEnv) = 'True
61 type instance MC.CanDo ClientConnection (MC.EffExcept ClientError) = 'True
62 type instance MC.CanDo ClientConnection (MC.EffExec IO) = 'True
63 instance MC.MonadExceptN 'MC.Zero ClientError ClientConnection where
64 throwN px = ClientConnection . lift . MC.throwN px
65 instance MC.MonadReaderN 'MC.Zero ClientEnv ClientConnection where
66 askN px = ClientConnection $ MC.askN px
67 instance MC.MonadExecN 'MC.Zero IO ClientConnection where
68 execN _px = ClientConnection . lift . lift
70 -- | Try clients in order, last error is preserved.
71 instance Alternative ClientConnection where
72 empty = MC.throw $ ClientError_EmptyClient
73 x <|> y = ClientConnection $ do
75 liftIO (runClientConnection env x) >>= \case
77 Left _err -> unClientConnection y
81 MimeTypes ts (MimeDecodable a) =>
82 (Proxy ts -> Proxy a -> ClientRequest) ->
84 clientConnection req = do
85 clientRes <- doClientRequest $ req (Proxy::Proxy ts) (Proxy::Proxy a)
86 clientResMimeDecode (Proxy::Proxy ts) clientRes
88 runClientConnection :: ClientEnv -> ClientConnection a -> IO (Either ClientError a)
89 runClientConnection env (ClientConnection c) = E.runExceptT $ R.runReaderT c env
91 runClientRequest :: ClientEnv -> ClientRequest -> IO (Either ClientError ClientResponse)
92 runClientRequest env = runClientConnection env . doClientRequest
94 -- ** Type 'ClientEnv'
97 { clientEnv_manager :: Client.Manager
98 , clientEnv_baseURI :: URI
99 , clientEnv_cookieJar :: Maybe (STM.TVar Client.CookieJar)
101 clientEnv :: Client.Manager -> URI -> ClientEnv
102 clientEnv clientEnv_manager clientEnv_baseURI =
104 { clientEnv_cookieJar = Nothing
108 -- ** Type 'ClientError'
110 -- | The server returned an error response
111 = ClientError_FailureResponse ClientResponse
112 -- | The body could not be decoded at the expected type
113 | ClientError_DecodeFailure Text ClientResponse
114 -- | The content-type of the response is not supported
115 | ClientError_UnsupportedContentType BS.ByteString ClientResponse
117 -- | The content-type header is invalid
118 | ClientError_InvalidContentTypeHeader ClientResponse
120 -- | There was a connection error, and no response was received
121 | ClientError_ClientConnectionectionError Client.HttpException
122 -- | 'ClientConnection' is 'empty'
123 | ClientError_EmptyClient
124 deriving (Eq, Show{-, Generic, Typeable-})
125 instance Exn.Exception ClientError
126 instance Eq Client.HttpException where
127 (==) = (==) `on` show
129 -- * Type 'ClientRequest'
132 { clientReqHttpVersion :: HTTP.HttpVersion
133 , clientReqMethod :: HTTP.Method
134 , clientReqPath :: BSB.Builder
135 , clientReqQueryString :: Seq HTTP.QueryItem
136 , clientReqAccept :: Seq Media.MediaType
137 , clientReqHeaders :: Seq HTTP.Header
138 , clientReqBody :: Maybe (Client.RequestBody, Media.MediaType)
140 instance Default ClientRequest where
142 { clientReqHttpVersion = HTTP.http11
143 , clientReqMethod = HTTP.methodGet
145 , clientReqQueryString = Seq.empty
146 , clientReqAccept = Seq.empty
147 , clientReqHeaders = Seq.empty
148 , clientReqBody = Nothing
150 instance Show ClientRequest where
151 show _ = "ClientRequest"
153 clientRequest :: URI -> ClientRequest -> Client.Request
154 clientRequest baseURI req =
155 Client.defaultRequest
156 { Client.method = clientReqMethod req
157 , Client.host = maybe "" (fromString . URI.uriRegName) $ URI.uriAuthority baseURI
158 , Client.port = case URI.uriPort <$> URI.uriAuthority baseURI of
159 Just (':':p) | Just port <- readMaybe p -> port
161 , Client.path = BSL.toStrict $ fromString (URI.uriPath baseURI) <> BSB.toLazyByteString (clientReqPath req)
162 , Client.queryString = HTTP.renderQuery True . toList $ clientReqQueryString req
163 , Client.requestHeaders = acceptHeader <> contentTypeHeader <> headers
165 , Client.secure = URI.uriScheme baseURI == "https"
168 headers = List.filter (\(h, _) -> h/="Accept" && h/="Content-Type") $
169 toList $ clientReqHeaders req
171 acceptHeader | null hs = []
172 | otherwise = [("Accept", Media.renderHeader hs)]
174 hs = toList $ clientReqAccept req
176 (requestBody, contentTypeHeader) =
177 case clientReqBody req of
178 Nothing -> (Client.RequestBodyLBS "", [])
179 Just (body, typ) -> (body, [(HTTP.hContentType, Media.renderHeader typ)])
181 setClientRequestBodyLBS :: BSL.ByteString -> MediaType -> ClientRequest -> ClientRequest
182 setClientRequestBodyLBS body mt req = req{ clientReqBody =
183 Just (Client.RequestBodyLBS body, mt) }
185 setClientRequestBody :: Client.RequestBody -> MediaType -> ClientRequest -> ClientRequest
186 setClientRequestBody body mt req = req{ clientReqBody = Just (body, mt) }
188 -- ** Type 'ClientResponse'
190 = ClientResponseWithBody BSL.ByteString
191 data ClientResponseWithBody a
193 { clientResStatus :: HTTP.Status
194 , clientResHeaders :: Seq HTTP.Header
195 , clientResHttpVersion :: HTTP.HttpVersion
197 } deriving (Eq, Show, Functor)
199 clientResponse :: Client.Response a -> ClientResponseWithBody a
202 { clientResStatus = Client.responseStatus res
203 , clientResBody = Client.responseBody res
204 , clientResHeaders = Seq.fromList $ Client.responseHeaders res
205 , clientResHttpVersion = Client.responseVersion res
208 doClientRequest :: ClientRequest -> ClientConnection ClientResponse
209 doClientRequest clientReq = do
210 ClientEnv{..} <- MC.ask
212 let req = clientRequest clientEnv_baseURI clientReq in
213 case clientEnv_cookieJar of
217 now <- Time.getCurrentTime
219 oldCookieJar <- STM.readTVar cj
220 let (newRequest, newCookieJar) =
221 Client.insertCookiesIntoRequest req oldCookieJar now
222 STM.writeTVar cj newCookieJar
225 liftIO $ catchClientConnectionectionError $
226 Client.httpLbs req clientEnv_manager
228 Left err -> MC.throw err
230 for_ clientEnv_cookieJar $ \cj ->
232 now <- Time.getCurrentTime
233 STM.atomically $ STM.modifyTVar' cj (fst . Client.updateCookieJar res req now)
234 let status = HTTP.statusCode $ Client.responseStatus res
235 clientRes = clientResponse res
236 unless (status >= 200 && status < 300) $
237 MC.throw $ ClientError_FailureResponse clientRes
240 catchClientConnectionectionError :: IO a -> IO (Either ClientError a)
241 catchClientConnectionectionError action =
242 Exn.catch (Right <$> action) $ \err ->
243 return $ Left $ ClientError_ClientConnectionectionError err
245 -- ** Type 'ClientResponseStreaming'
246 newtype ClientResponseStreaming
247 = ClientResponseStreaming
248 { runClientResponseStreaming ::
250 (ClientResponseWithBody (IO BS.ByteString) -> IO a) ->
254 doClientRequestStreaming :: ClientRequest -> ClientConnection ClientResponseStreaming
255 doClientRequestStreaming clientReq = do
256 ClientEnv{..} <- MC.ask
257 let req = clientRequest clientEnv_baseURI clientReq
258 return $ ClientResponseStreaming $ \k ->
259 Client.withResponse req clientEnv_manager $ \res -> do
260 let status = HTTP.statusCode $ Client.responseStatus res
261 unless (status >= 200 && status < 300) $ do
262 responseBody <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)
263 Exn.throw $ ClientError_FailureResponse $ clientResponse res{Client.responseBody}
264 k $ clientResponse res
267 clientResContentType ::
268 MC.MonadExcept ClientError m =>
269 ClientResponse -> m MediaType
270 clientResContentType clientRes =
271 case List.lookup "Content-Type" $ toList $ clientResHeaders clientRes of
272 Nothing -> return $ "application"Media.//"octet-stream"
274 case Media.parseAccept mt of
275 Nothing -> MC.throw $ ClientError_InvalidContentTypeHeader clientRes
276 Just mt' -> return mt'
279 clientResMimeDecode ::
281 MimeTypes ts (MimeDecodable a) =>
282 MC.MonadExcept ClientError m =>
283 Proxy ts -> ClientResponse -> m a
284 clientResMimeDecode Proxy clientRes = do
286 fromMaybe "application/octet-stream" $
287 List.lookup "Content-Type" $ toList $ clientResHeaders clientRes
288 case matchContent @ts @(MimeDecodable a) mtRes of
289 Nothing -> MC.throw $ ClientError_UnsupportedContentType mtRes clientRes
290 Just (MimeType mt) ->
291 case mimeDecode mt $ clientResBody clientRes of
292 Left err -> MC.throw $ ClientError_DecodeFailure (T.pack err) clientRes
293 Right val -> return val
296 liftIO :: MC.MonadExec IO m => IO a -> m a
298 {-# INLINE liftIO #-}